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
One run, call by callimage_upscale
  1. POST/v1/files/upload-url201
  2. POST/v1/files/{file_id}/complete200
  3. POST/v1/tools/image_upscale/estimate200
  4. POST/v1/tools/image_upscale/runs202 queued
  5. GET/v1/runs/{run_id}succeeded
  6. 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.

  1. 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.

    Open Settings › API keys

  2. Step 2: Check the key

    Send it as a bearer token. /v1/me tells 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
    }
  3. 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_id to reuse in estimates and runs. The three calls are in File uploads.

  4. Step 4: Check the price

    An estimate starts nothing and spends nothing. sparks is the expected price; reserve_sparks is 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
    }
  5. 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"]}}'
  6. 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:

A key reads stai_live_, then a 8-character prefix, an underscore and a 32-character secret.
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_at says when the month starts again. GET /v1/me shows the key's monthly_sparks_cap and its sparks_this_month.
  • Keys work on Plus, Pro and Team. If the team moves to Free, every call answers 403 plan_upgrade_required until 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 onread
List tools, estimate, read runs, download outputs, GET /v1/me.
Defaultrun
Start and cancel runs, upload files.
Opt inwebhooks
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.

429Response
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 imageimage_upscale
image→ image
Video transitionvideo_transition
clips, style (optional): fade, dissolve, wipe_left, wipe_right, duration_seconds (optional)→ video
Colour gradeimage_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 videovideo_upscale
video, target (optional): 1080p, 4k→ video
Extract brand kitbrand_kit_extract
url (optional), files (optional), save (optional)→ json
Musicmusic_generate
prompt, length (optional): clip, song, exact, instrumental (optional), lyrics (optional), seconds (optional), images (optional)→ audio
Sound effectsound_effect_generate
prompt, seconds (optional)→ audio
Add soundvideo_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.

Run statuses: queued, then running, then one of succeeded, failed or canceled. A queued run can also be canceled.
  • 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_sparks is 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 pass next_cursor as cursor for the next page. Filter by status (repeat it for several), tool_id, api_key_id, created_after and created_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.
The run objectExample values
{
  "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.

  1. POST /v1/files/upload-url with the file's name, type and size. You get a file_id, an upload_id, the part size and one URL per part.
  2. PUT each part to its URL and keep the ETag response header.
  3. POST /v1/files/{file_id}/complete with 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

What your endpoint receives
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:

readlist_tools
The tools your team can run, with their inputs and a price range.
readestimate
The price of exact inputs in Sparks. Spends nothing.
runrun_tool
Starts a run. Without confirm: true it starts nothing and answers with the price.
readget_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.
readlist_recent_runs
Your team's newest runs started with an API key or a connected app.
runupload_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.
readget_workflow_instructions
How to fill one tool's inputs, with an example.
  • run_tool starts nothing without confirm: true. It answers confirmation_required with 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

  1. 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.
  2. Enter https://api.smiletoai.com/mcp as the server URL and add the connector.
  3. Connect it, sign in with your SmileToAI account, pick a team and choose Allow access.
  4. 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 Code
claude mcp add --transport http smiletoai https://api.smiletoai.com/mcp

For a script or a machine with no browser, pass a team API key on the command line instead:

Claude Code, API key
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.

mcp.json
{
  "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 body
{
  "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.

401api_key_required
No key, or a session token instead of a key.
401invalid_api_key
An unknown, malformed or revoked key, a key from another environment, or a key whose creator left the team.
402insufficient_sparks
The run's hold is more than the team's available Sparks.
402spend_cap_reached
The run's hold would take the key past its monthly Sparks cap. details has cap_sparks, used_sparks, requested_sparks and resets_at.
403plan_upgrade_required
The team is on Free, or the tool needs a paid plan.
403consent_required
An identity tool's element has no active consent.
403insufficient_scope
The key lacks the scope named in details.required_scope.
404tool_not_found
No tool with that id is available over the API.
404run_not_found
An unknown run id, or a run your team's keys did not start.
404output_not_found
Not an output of that run, or no longer available.
409tool_execution_pipeline
The tool runs as a multi-step pipeline, which the API does not start.
409output_not_ready
The output is still being checked. Try again in a moment.
422invalid_request
An input is missing or invalid; errors[] names each one.
429rate_limited
Over the key's limit or the team's hourly run ceiling. Wait for Retry-After.
500internal_error
Something failed on our side. Retry with backoff, and quote request_id if 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.
Compare plans

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.