DevelopersAPI guide
Build with the SmileToAI API
Run SmileToAI tools from your own code, or from Claude, Cursor and other MCP clients. Upload a file, check the price, start a run, then poll it or take a signed webhook. Runs spend your team's Sparks at the same rates as the app.
- Base URL
- api.smiletoai.com/v1
- Auth
- Bearer team key
- Plans
- Plus, Pro and Team
- Spec
- OpenAPI 3.1
- POST/v1/files/upload-url201
- POST/v1/files/{file_id}/complete200
- POST/v1/tools/image_upscale/estimate200
- POST/v1/tools/image_upscale/runs202 queued
- GET/v1/runs/{run_id}succeeded
- GET/v1/runs/{run_id}/outputs/{asset_id}/url200
Or skip the polling: a signed run.succeeded webhook arrives when the run ends.
Quickstart
Six steps from a new key to a finished file. The example upscales an image with image_upscale; every tool uses the same calls.
Step 1: Create an API key
In SmileToAI, open Settings › API keys and create a key. Owners and admins can create keys on Plus, Pro and Team. The full key is shown once, so put it in your secret manager before you close the dialog.
Step 2: Check the key
Send it as a bearer token.
/v1/metells you which team, plan and key you are using.export SMILETOAI_KEY="stai_live_..." curl https://api.smiletoai.com/v1/me \ -H "Authorization: Bearer $SMILETOAI_KEY"200Response { "object": "me", "team": { "id": "6f0c2d9a-3b1e-4f57-9a0d-2c8e5b7f1a64", "name": "Northwind Studio", "plan": "pro" }, "api_key": { "id": "a3e1b0c4-5d2f-4e8a-b7c9-0f1e2d3c4b5a", "name": "Production", "prefix": "k7m2p9qa", "scopes": [ "read", "run" ], "rate_limit_per_minute": 60, "monthly_sparks_cap": 20000, "sparks_this_month": 4120, "month_resets_at": "2026-11-01T00:00:00Z" }, "sparks_available": 84210 }Step 3: Upload the image
Media inputs take file ids, not URLs. Ask for upload URLs, PUT the bytes, then complete the file; you get a
file_idto reuse in estimates and runs. The three calls are in File uploads.Step 4: Check the price
An estimate starts nothing and spends nothing.
sparksis the expected price;reserve_sparksis what a run holds while it works.curl -X POST https://api.smiletoai.com/v1/tools/image_upscale/estimate \ -H "Authorization: Bearer $SMILETOAI_KEY" \ -H "Content-Type: application/json" \ -d '{"inputs": {"image": ["9d41e2c7-0b8a-4f3e-a1d5-6c7b8e9f0a12"]}}'200Response { "object": "estimate", "tool_id": "image_upscale", "product_id": "image_upscale", "sparks": 30, "reserve_sparks": 30 }Step 5: Start the run
The API answers 202 with a queued run. Send an
Idempotency-Key: if you retry the request, you get the same run back instead of a second one.curl -X POST https://api.smiletoai.com/v1/tools/image_upscale/runs \ -H "Authorization: Bearer $SMILETOAI_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-1042" \ -d '{"inputs": {"image": ["9d41e2c7-0b8a-4f3e-a1d5-6c7b8e9f0a12"]}}'Step 6: Get the result
Poll every 2 to 5 seconds until the status is
succeeded, then ask for a download URL. Long video runs are easier with a webhook.export RUN_ID="8f2c0b7e-41d6-4c1a-9e55-2b7d1f0c9a31" # Repeat every few seconds until "status" is succeeded, failed or canceled curl https://api.smiletoai.com/v1/runs/$RUN_ID \ -H "Authorization: Bearer $SMILETOAI_KEY" # Then ask for a short-lived download URL for an output curl https://api.smiletoai.com/v1/runs/$RUN_ID/outputs/$ASSET_ID/url \ -H "Authorization: Bearer $SMILETOAI_KEY"
Authentication and keys
Every request carries a team API key in the Authorization header: Authorization: Bearer stai_live_…. A key has three parts:
- Environment
- stai_live_ in production. Keys made outside production start with stai_test_ and only work there.
- Prefix
- 8 characters, listed in Settings so you can tell keys apart.
- Secret
- 32 characters, shown once. Only a hash is kept, so a lost key cannot be recovered.
- A key belongs to one team and acts as the member who created it. Its runs count toward that member in Settings › Team.
- If that member leaves the team, the key stops working, and re-joining does not bring it back. Create a new key.
- Owners and admins create and revoke keys in Settings › API keys, up to 25 active keys per team. A key cannot create or revoke keys.
- Owners and admins can also give a key a monthly Sparks cap. Once the runs a key started in a calendar month (UTC) hold or have spent the cap, its next run answers
402 spend_cap_reached;details.resets_atsays when the month starts again.GET /v1/meshows the key'smonthly_sparks_capand itssparks_this_month. - Keys work on Plus, Pro and Team. If the team moves to Free, every call answers
403 plan_upgrade_requireduntil it upgrades. - Every key starts with
stai_, so secret scanners can spot a leaked SmileToAI key.
Keep keys on your server. Never put one in a browser or a mobile app: anyone who has it can spend your team's Sparks. If a key leaks, revoke it in Settings › API keys.
Scopes
New keys get read and run. A call outside a key's scopes answers 403 insufficient_scope.
- Always on
read - List tools, estimate, read runs, download outputs,
GET /v1/me. - Default
run - Start and cancel runs, upload files.
- Opt in
webhooks - Add, change, test and delete webhook endpoints. The key's creator must be an owner or admin.
Rate limits
Each key has its own limit, chosen when you create it. Requests draw from a bucket that holds a minute's worth, so short bursts are fine.
- 30per min
- 60per min · default
- 120per min
- 300per min
Over the limit, the API answers 429 rate_limited with a Retry-After header in seconds. Your team also has an hourly ceiling on new runs, shared by every key and the app; it answers 429 the same way, with Retry-After pointing at the next hour.
HTTP/1.1 429 Too Many Requests
Retry-After: 4
X-Request-Id: req_5c1e9a0b7d214e33
{
"error": {
"code": "rate_limited",
"message": "Rate limit of 60 requests per minute exceeded; retry in 4s.",
"request_id": "req_5c1e9a0b7d214e33",
"details": {
"limit_per_minute": 60
}
}
}Tools and estimates
GET /v1/tools lists what your key can run, with each tool's inputs and a price range. These are the tools on the API today:
- Upscale image
image_upscale image→ image- Video transition
video_transition clips,style(optional): fade, dissolve, wipe_left, wipe_right,duration_seconds(optional)→ video- Colour grade
image_color_grade image,temperature(optional),contrast(optional),saturation(optional),exposure(optional),grain(optional),preset(optional): natural, warm_film, cool_matte, bw_16mm, none→ image- Upscale video
video_upscale video,target(optional): 1080p, 4k→ video- Extract brand kit
brand_kit_extract url(optional),files(optional),save(optional)→ json- Music
music_generate prompt,length(optional): clip, song, exact,instrumental(optional),lyrics(optional),seconds(optional),images(optional)→ audio- Sound effect
sound_effect_generate prompt,seconds(optional)→ audio- Add sound
video_add_sound video,prompt(optional),versions(optional): 1, 2→ video
Before you run, POST /v1/tools/{tool_id}/estimate prices the exact inputs you will send. Add duration_seconds for video tools. A tool your plan does not include lists with "allowed": false.
Runs and polling
A run moves through these statuses. The last three are final.
- Poll
GET /v1/runs/{run_id}no faster than every 2 to 5 seconds, stop at a final status, and back off on a 429. cost_sparksis the hold while the run works and the final charge once it ends. A failed or canceled run releases its hold.- Cancel a queued or running run with
POST /v1/runs/{run_id}/cancel. - List your team's runs, newest first, with
GET /v1/runs: up to 100 per page (limit, default 20), then passnext_cursorascursorfor the next page. Filter bystatus(repeat it for several),tool_id,api_key_id,created_afterandcreated_before. - Download URLs are short-lived. Fetch the file straight away, and ask for a new URL if one expires.
- You can read runs started with your team's keys. Runs from the app are not visible here.
{
"object": "run",
"id": "8f2c0b7e-41d6-4c1a-9e55-2b7d1f0c9a31",
"tool_id": "image_upscale",
"product_id": "image_upscale",
"status": "succeeded",
"progress": 1,
"cost_sparks": 30,
"outputs": [
{
"asset_id": "c71a5e02-9b4d-4e1f-8a3c-5d6e7f8091a2",
"mime_type": "image/png",
"width": 4096,
"height": 4096,
"duration_seconds": null,
"size_bytes": 9420117,
"file_name": "photo-upscaled.png"
}
],
"error": null,
"api_key_id": "a3e1b0c4-5d2f-4e8a-b7c9-0f1e2d3c4b5a",
"created_at": "2026-10-17T09:12:03Z",
"started_at": "2026-10-17T09:12:05Z",
"finished_at": "2026-10-17T09:12:41Z"
}File uploads
Files go straight to storage in parts, so large videos upload without passing through the API.
POST /v1/files/upload-urlwith the file's name, type and size. You get afile_id, anupload_id, the part size and one URL per part.- PUT each part to its URL and keep the
ETagresponse header. POST /v1/files/{file_id}/completewith the upload id and every part's number and ETag.
# 1. Ask for upload URLs
SIZE=$(stat -c%s photo.png)
curl -s -X POST https://api.smiletoai.com/v1/files/upload-url \
-H "Authorization: Bearer $SMILETOAI_KEY" \
-H "Content-Type: application/json" \
-d "{\"file_name\":\"photo.png\",\"mime_type\":\"image/png\",\"size_bytes\":$SIZE}" > upload.json
FILE_ID=$(jq -r .file_id upload.json)
# 2. PUT the part and keep its ETag
ETAG=$(curl -s -D - -o /dev/null -X PUT --upload-file photo.png \
"$(jq -r '.parts[0].url' upload.json)" | awk 'tolower($1)=="etag:"{print $2}' | tr -d '\r')
# 3. Complete the file
curl -s -X POST https://api.smiletoai.com/v1/files/$FILE_ID/complete \
-H "Authorization: Bearer $SMILETOAI_KEY" \
-H "Content-Type: application/json" \
-d "{\"upload_id\":\"$(jq -r .upload_id upload.json)\",\"parts\":[{\"part_number\":1,\"etag\":$ETAG}]}"Then pass the id in a media input, for example "image": ["9d41e2c7-0b8a-4f3e-a1d5-6c7b8e9f0a12"].
Webhooks
Instead of polling, let SmileToAI call your server when a run ends. Owners and admins add endpoints in Settings › API keys on the web, or with a key that has the webhooks scope. A team can have up to 10, on HTTPS.
run.succeeded- A run finished and its outputs are ready.
run.failed- A run stopped with an error. Its hold is released.
run.canceled- A run was canceled. Its hold is released.
ping- Sent when you press Send test.
Only runs started with your team's API keys send events. An endpoint receives the runs that finish while it is enabled, and nothing is replayed: after a pause, or for a new endpoint, read any run you missed with GET /v1/runs/{run_id}.
A delivery
POST /hooks/smiletoai HTTP/1.1
Content-Type: application/json
User-Agent: SmileToAI-Webhooks/1
SmileToAI-Event: run.succeeded
SmileToAI-Delivery: 2b9e47d1-6a5c-4f8b-9e0d-3c2b1a0f9e8d
SmileToAI-Signature: t=1792230761,v1=5f8e0c4b…
{
"id": "evt_4c1d9be07a2f",
"object": "event",
"type": "run.succeeded",
"created_at": "2026-10-17T09:12:42Z",
"data": {
"run": {
"object": "run",
"id": "8f2c0b7e-41d6-4c1a-9e55-2b7d1f0c9a31",
"tool_id": "image_upscale",
"product_id": "image_upscale",
"status": "succeeded",
"progress": 1,
"cost_sparks": 30,
"outputs": [
{
"asset_id": "c71a5e02-9b4d-4e1f-8a3c-5d6e7f8091a2",
"mime_type": "image/png",
"width": 4096,
"height": 4096,
"duration_seconds": null,
"size_bytes": 9420117,
"file_name": "photo-upscaled.png"
}
],
"error": null,
"api_key_id": "a3e1b0c4-5d2f-4e8a-b7c9-0f1e2d3c4b5a",
"created_at": "2026-10-17T09:12:03Z",
"started_at": "2026-10-17T09:12:05Z",
"finished_at": "2026-10-17T09:12:41Z"
}
}
}Retries
Answer with any 2xx within 10 seconds. Anything else, including a redirect, is tried again on this schedule:
- 1 min
- 5 min
- 30 min
- 2 h
- 12 h
- then given up
Every retry sends the same body and event id, so store the ids you have handled and skip repeats. Pausing an endpoint drops its pending deliveries.
Verify the signature
v1 is an HMAC-SHA256 of t, a dot and the raw body, keyed with the endpoint's signing secret (whsec_…, shown once when you add it). Check it against the raw bytes before you parse JSON, and reject old timestamps.
import crypto from "node:crypto";
// header: the SmileToAI-Signature header. rawBody: the request body as a Buffer, before JSON.parse.
export function verifySmileToAI(secret, header, rawBody, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(",").map(item => item.split("=")));
const timestamp = Number(parts.t);
if (!Number.isFinite(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) {
return false;
}
const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.`).update(rawBody).digest();
const given = Buffer.from(parts.v1 ?? "", "hex");
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
}Use SmileToAI from Claude, Cursor and other MCP clients
SmileToAI is also a remote MCP server, so assistants that support the Model Context Protocol can list your tools, price a run and start it from a chat. Add https://api.smiletoai.com/mcp as a connector and sign in with your SmileToAI account: you pick a team and allow access, and never paste a key. A client that cannot sign in can send a team API key as the bearer token instead: Authorization: Bearer stai_live_…. The same scopes and rate limits apply to both, and each tool call counts as one request.
The server offers seven tools, each listed with the scope it needs:
- read
list_tools - The tools your team can run, with their inputs and a price range.
- read
estimate - The price of exact inputs in Sparks. Spends nothing.
- run
run_tool - Starts a run. Without
confirm: trueit starts nothing and answers with the price. - read
get_run - A run's status, and a download link for each output once it succeeds. The link is on SmileToAI's own domain and works for 7 days, until the app is disconnected or the file deleted. Anyone with the link can open the file.
- read
list_recent_runs - Your team's newest runs started with an API key or a connected app.
- run
upload_asset - Gets a file in. In ChatGPT a file you attach goes straight in, and any assistant can pass a public image, video or audio URL as
source_url. Otherwise, in a chat app it returns a SmileToAI upload link: you open it, drop the file, and the assistant reads the asset id back. A client that can send HTTP requests, such as a coding agent, can instead start, PUT the parts, then finish with their ETags. - read
get_workflow_instructions - How to fill one tool's inputs, with an example.
run_toolstarts nothing withoutconfirm: true. It answersconfirmation_requiredwith the price, so the assistant has to ask you first.- Finished outputs land in your team's Library, tagged
mcp. - An MCP run is also an API run:
GET /v1/runs/{run_id}reads it, it counts in the team's Sparks by member in Settings › Team, and webhooks fire for it. - Signing in works for any member of a paid team, and the connection acts as that member and spends the team you picked. The sign-in page lets you turn off starting runs, and it does not let you pick a Free team. A connection whose team later drops to Free, like a key, connects and lists the tools, but every tool call answers
plan_upgrade_required. - Manage or disconnect connections in Settings › API keys › Connected apps. Members disconnect their own, owners and admins any, and the assistant's next call is refused at once. API keys are created by owners and admins only.
Claude
- Open Customize › Connectors and choose Add custom connector. On Team and Enterprise, an Owner adds it in Organization settings › Connectors, and each member then connects with their own account.
- Enter
https://api.smiletoai.com/mcpas the server URL and add the connector. - Connect it, sign in with your SmileToAI account, pick a team and choose Allow access.
- Turn the connector on in a chat under + › Connectors, and ask Claude to list the SmileToAI tools.
To use a team API key instead, choose No sign-in and, under Request headers, add authorization with the value Bearer stai_live_…. Claude sends the value as typed, so include Bearer. Request headers are a Claude beta that not every organization has, and on Team and Enterprise a header the Owner sets is shared: every member who connects acts as the key's creator and spends the same team's Sparks. Signing in avoids both.
ChatGPT
Turn on developer mode in ChatGPT's settings, add a custom connector (an app that uses an MCP server) with https://api.smiletoai.com/mcp as the server URL, and choose OAuth as its authentication. Sign in with your SmileToAI account, pick a team and choose Allow access. Which ChatGPT plans can add a custom connector is up to OpenAI. A file you attach in ChatGPT reaches SmileToAI directly, with no upload link.
From your own code, the OpenAI Responses API's remote MCP tool can send a team API key as a header instead.
Claude Code
Add the server, then run /mcp inside Claude Code and choose SmileToAI to sign in. Your browser opens the SmileToAI sign-in page. Add --scope user to use SmileToAI in every project.
claude mcp add --transport http smiletoai https://api.smiletoai.com/mcpFor a script or a machine with no browser, pass a team API key on the command line instead:
export SMILETOAI_KEY="stai_live_..."
claude mcp add --transport http smiletoai https://api.smiletoai.com/mcp \
--header "Authorization: Bearer $SMILETOAI_KEY"Cursor
Cursor takes a team API key. Add SmileToAI to ~/.cursor/mcp.json for every project, or to .cursor/mcp.json for one. Cursor reads the key from your environment.
{
"mcpServers": {
"smiletoai": {
"url": "https://api.smiletoai.com/mcp",
"headers": {
"Authorization": "Bearer ${env:SMILETOAI_KEY}"
}
}
}
}Other clients
Any client that supports streamable HTTP works with the URL: a client that follows the MCP authorization flow opens the SmileToAI sign-in page, and any client that can send custom headers works with the Authorization header and a team API key. To try the server by hand, run the MCP Inspector (npx @modelcontextprotocol/inspector) and choose the Streamable HTTP transport.
Errors
Every error has the same shape, and every response carries an X-Request-Id header. Quote it when you contact support.
{
"error": {
"code": "insufficient_sparks",
"message": "The team does not have enough Sparks for this run.",
"request_id": "req_7d0a41c2e9b84f61"
}
}A 422 adds errors[], one entry per bad input. Some codes add details.
- 401
api_key_required - No key, or a session token instead of a key.
- 401
invalid_api_key - An unknown, malformed or revoked key, a key from another environment, or a key whose creator left the team.
- 402
insufficient_sparks - The run's hold is more than the team's available Sparks.
- 402
spend_cap_reached - The run's hold would take the key past its monthly Sparks cap.
detailshascap_sparks,used_sparks,requested_sparksandresets_at. - 403
plan_upgrade_required - The team is on Free, or the tool needs a paid plan.
- 403
consent_required - An identity tool's element has no active consent.
- 403
insufficient_scope - The key lacks the scope named in
details.required_scope. - 404
tool_not_found - No tool with that id is available over the API.
- 404
run_not_found - An unknown run id, or a run your team's keys did not start.
- 404
output_not_found - Not an output of that run, or no longer available.
- 409
tool_execution_pipeline - The tool runs as a multi-step pipeline, which the API does not start.
- 409
output_not_ready - The output is still being checked. Try again in a moment.
- 422
invalid_request - An input is missing or invalid;
errors[]names each one. - 429
rate_limited - Over the key's limit or the team's hourly run ceiling. Wait for
Retry-After. - 500
internal_error - Something failed on our side. Retry with backoff, and quote
request_idif it keeps happening.
Pricing in Sparks
The API has no price of its own. A run costs the same Sparks as the same tool in the app, from the same balance.
- Included in Plus, Pro and Team. Free teams cannot create keys.
- Check any price with an estimate before you run.
- See spend per key in Settings › API keys (last 30 days) and per member in Settings › Team.
Versioning
This is version 1. Every path starts with /v1, and `/v1/openapi.json` always describes what is live.
Within v1 we add things without notice: new tools, new optional fields, new error codes and new event types, so ignore fields you do not know. A change that would break existing calls ships as a new version, and we announce it before v1 changes.