Two endpoints create images:
POST /v1/images/generationsgenerates images from a text prompt.POST /v1/images/editsedits one or more reference images with a text prompt.
Both endpoints need an API key sent as Authorization: Bearer. See Authentication. The body is JSON. Multipart uploads are not supported, so you send reference images as URLs or data URIs inside the JSON.
Every image request runs as a job. The request can wait for the job and return the images in the response, or return the job at once so you can collect the result later.
Generate an image
Section titled “Generate an image”Send the model and a prompt. The example uses sakuranbo/cherryimage-1.0-pro.
curl -X POST "https://api.sakuranbo.ai/v1/images/generations" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $SAKURANBO_API_KEY" \ -d '{ "model": "sakuranbo/cherryimage-1.0-pro", "prompt": "A matte ceramic coffee mug on a walnut desk, soft morning light, shallow depth of field", "resolution": "2k", "aspect_ratio": "4:3"}'const response = await fetch("https://api.sakuranbo.ai/v1/images/generations", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.SAKURANBO_API_KEY}`, }, body: JSON.stringify({ "model": "sakuranbo/cherryimage-1.0-pro", "prompt": "A matte ceramic coffee mug on a walnut desk, soft morning light, shallow depth of field", "resolution": "2k", "aspect_ratio": "4:3" }),});const data = await response.json();console.log(data);import os
import requests
response = requests.post( "https://api.sakuranbo.ai/v1/images/generations", headers={ "Authorization": f"Bearer {os.environ['SAKURANBO_API_KEY']}", }, json={ "model": "sakuranbo/cherryimage-1.0-pro", "prompt": "A matte ceramic coffee mug on a walnut desk, soft morning light, shallow depth of field", "resolution": "2k", "aspect_ratio": "4:3", },)print(response.json())These are the input parameters of the example model:
| Name | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|
prompt | string | Yes | Up to 5,000 characters | Text description of the image to generate or the edit to apply. | |
images | url[] | No | Up to 10 items | Reference images for editing (1 to 10). Required on /v1/images/edits. | |
resolution | enum | No | 2k | One of: 1k, 1.5k, 2k | Output resolution. Determines the per-image price. |
n | integer | No | 1 | 1 to 100 | Number of images to generate (1 to 100). The price is the per-image price times this value. |
aspect_ratio | enum | No | auto | One of: auto, 1:1, 4:3, 3:4, 16:9, 9:16, 3:2, 2:3, 21:9 | Aspect ratio of the output image. |
output_format | enum | No | jpeg | One of: jpeg, png | Image file format. |
The request body has these fields:
| Name | Type | Required | Default | Constraints |
|---|---|---|---|---|
model | string | Yes | 1 to 128 characters | |
prompt | string | Yes | ||
n | number | No | ||
resolution | string | No | ||
aspect_ratio | string | No | ||
output_format | string | No | ||
response_format | string | No | url | One of: url, b64_json |
resolution selects the price. A parameter that is not in the table, or a value outside its range, returns 400 with invalid_request. error.param names the parameter, and error.details.allowed, error.details.minimum, or error.details.maximum give the valid values.
Wait for the result
Section titled “Wait for the result”By default the request waits for the images.
- If the job finishes within the maximum wait time, the response is
200with the images. The wait time is stated in the API reference. - If the job is still running after that time, the response is
202with the job and aLocationheader that points to it. The generation keeps running. - If you send
Prefer: respond-async, the response is202at once, and the response has the headerPreference-Applied: respond-async.
curl -X POST "https://api.sakuranbo.ai/v1/images/generations" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $SAKURANBO_API_KEY" \ -H "Prefer: respond-async" \ -d '{ "model": "sakuranbo/cherryimage-1.0-pro", "prompt": "A matte ceramic coffee mug on a walnut desk, soft morning light, shallow depth of field", "resolution": "2k", "aspect_ratio": "4:3"}'const response = await fetch("https://api.sakuranbo.ai/v1/images/generations", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.SAKURANBO_API_KEY}`, Prefer: "respond-async", }, body: JSON.stringify({ "model": "sakuranbo/cherryimage-1.0-pro", "prompt": "A matte ceramic coffee mug on a walnut desk, soft morning light, shallow depth of field", "resolution": "2k", "aspect_ratio": "4:3" }),});const data = await response.json();console.log(data);import os
import requests
response = requests.post( "https://api.sakuranbo.ai/v1/images/generations", headers={ "Authorization": f"Bearer {os.environ['SAKURANBO_API_KEY']}", "Prefer": "respond-async", }, json={ "model": "sakuranbo/cherryimage-1.0-pro", "prompt": "A matte ceramic coffee mug on a walnut desk, soft morning light, shallow depth of field", "resolution": "2k", "aspect_ratio": "4:3", },)print(response.json())After a 202 response, collect the result in one of two ways. Poll the job as described in Jobs, or receive a job.succeeded, job.failed, or job.cancelled event as described in Webhooks.
If the job fails while the request is waiting, the response is an error and not a 202, and error.details.job_id is the ID of the job. After a 202 response, a failure shows in the error field of the job instead, and the request that reads the job still returns 200.
A 200 response has these fields:
| Name | Type |
|---|---|
created | integer |
data | object[] |
usage | object |
cost | object |
job_id | string |
cost is the final charge. The same amount is in the X-Usage-Cost-Usd header. job_id is the ID of the job that made the images.
Output files
Section titled “Output files”Set response_format to choose how the images come back.
urlis the default. Each item indatahas aurland anexpires_at. The URL is signed. It needs no API key and works untilexpires_at, which is 7 days after the job completes. Anyone who has the URL can download the image until then, so treat the URL as a secret. Afterexpires_atthe file is deleted, so save the images you need.b64_jsonputs the image in the response body as base64. If the images are too large to inline, the images that do not fit come back asurlitems.
data has one item for each image that was delivered. If part of a batch fails, data can have fewer items than n, and you pay for the images that were delivered.
Edit an image
Section titled “Edit an image”POST /v1/images/edits takes the same fields as the generation endpoint, and the
images field is required.
curl -X POST "https://api.sakuranbo.ai/v1/images/edits" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $SAKURANBO_API_KEY" \ -d '{ "model": "sakuranbo/cherryimage-1.0-pro", "prompt": "Make the mug dark blue", "images": [ "https://example.com/reference.png" ]}'const response = await fetch("https://api.sakuranbo.ai/v1/images/edits", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.SAKURANBO_API_KEY}`, }, body: JSON.stringify({ "model": "sakuranbo/cherryimage-1.0-pro", "prompt": "Make the mug dark blue", "images": [ "https://example.com/reference.png" ] }),});const data = await response.json();console.log(data);import os
import requests
response = requests.post( "https://api.sakuranbo.ai/v1/images/edits", headers={ "Authorization": f"Bearer {os.environ['SAKURANBO_API_KEY']}", }, json={ "model": "sakuranbo/cherryimage-1.0-pro", "prompt": "Make the mug dark blue", "images": [ "https://example.com/reference.png", ], },)print(response.json())images is a list of reference images. Each item is a public https URL or a data:image/...;base64, URI of a PNG, JPEG, WebP, or GIF image. The order matters, because the prompt can refer to the first image or the second image. A data URI has a smaller size limit than a URL, so send a large image as a public URL.
The server fetches each URL to check it before it creates the job. It does not follow redirects, and it rejects URLs that have credentials, IP addresses, or internal host names. An image that cannot be used returns 400 with invalid_request. error.param is images followed by the index of the item. For a URL, error.details.reason says what is wrong. For a data URI that is too large, error.details.max_bytes is the limit.
| Name | Type | Required | Default | Constraints |
|---|---|---|---|---|
model | string | Yes | 1 to 128 characters | |
prompt | string | Yes | ||
n | number | No | ||
resolution | string | No | ||
aspect_ratio | string | No | ||
output_format | string | No | ||
response_format | string | No | url | One of: url, b64_json |
images | string[] | Yes | 1 to 10 items |
The response is the same as for generation.
Retry safely
Section titled “Retry safely”Send an Idempotency-Key header with a value you choose. If a request with the same key and the same content arrives again, you get the same job and you are charged once. The same key with a different request returns 409 with idempotency_conflict. The key is remembered for a limited time, which is stated in the API reference.
Billing
Section titled “Billing”When a request is accepted, Sakuranbo AI reserves the price of the images you asked for from your balance. When the job finishes, the charge is calculated for the images that were delivered, and the rest of the reservation returns to your balance. A job that is cancelled, expires, or is rejected is not billed. A failed job is normally not billed either, and its reservation returns to your balance.
In rare cases, Sakuranbo AI cannot confirm whether the request reached the model provider. The job then fails with upstream_error, and its reserved amount stays reserved until Sakuranbo AI confirms whether the request is billed. If it stays reserved, contact support with the job ID.
The price depends on the model, the resolution, and the number of images. See the model catalog and the pricing page, or calculate it before you run the request with POST /v1/estimate. See the Estimate API.
Content policy
Section titled “Content policy”The model provider can reject the prompt, a reference image, or a generated image because of its content. The job then fails with content_policy_violation, and nothing is charged. A request that is waiting for the job returns 422 with this code, and error.details.job_id is the ID of the job. After a 202 response, the code is in the error field of the job.
Errors
Section titled “Errors”| Code | HTTP status | Type | Description |
|---|---|---|---|
invalid_request | 400 | invalid_request_error | The request is malformed or a value is out of range. details.allowed lists the valid values when they are known. |
unauthorized | 401 | authentication_error | The API key is missing, malformed, unknown, revoked, or expired. The response does not say which. |
balance_insufficient | 402 | insufficient_balance | The available balance is lower than the amount that must be reserved for this request. error.details.limit_source is balance. |
key_limit_exceeded | 402 | insufficient_balance | This request would take the API key over its spending limit. error.details.limit_source is key, and error.details.resets_at says when the counter starts again. |
account_suspended | 403 | permission_error | The account is suspended, so image, video, and chat requests are refused. Contact support. The balance, deposits, API keys, and jobs stay available. |
model_not_found | 404 | not_found_error | The model ID does not exist in the catalog. The chat endpoints also return it for a model that is not a text model. |
idempotency_conflict | 409 | conflict_error | The Idempotency-Key was already used with a different request. |
payload_too_large | 413 | invalid_request_error | The request body is larger than the allowed size. error.details.max_bytes is the limit. |
content_policy_violation | 422 | content_policy_error | The model provider rejected the prompt, a reference image, or the generated output because of its content. The request is not billed. |
rate_limited | 429 | rate_limit_error | The model provider is rate limiting requests. POST /v1/reports also returns this code when too many reports come from one address. Retry after the time given in the Retry-After header. |
concurrency_limited | 429 | rate_limit_error | The model provider is busy and cannot take this request right now. It is not a limit on the number of jobs in your account. Retry later. |
upstream_error | 502 | upstream_error | The upstream provider returned an error. |
upstream_unavailable | 503 | overloaded_error | The upstream provider is temporarily out of capacity. Retry later. |
POST /v1/images/edits returns the same codes. For a 429 or 503 response, wait the
number of seconds in the Retry-After header before you retry. A 402 response has
error.details.limit_source, which is balance or key. See Balance and key
limits. For the full list of codes, see
Errors.
All prices are in USD. Listed prices include our margin.