DevelopersAPI guide
Build with the SmileToAI API
Run SmileToAI tools from your own code. 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 }, "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.
- 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
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. - 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);
}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.
- 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.