# Samsa API
Source: https://docs.samsa.ai/api
Generate images, edit and transform them, produce video, and train custom models — programmatically, from your organization's Samsa account.
The **Samsa API** brings Samsa's generative image and video studio to your own
applications. Authenticate with an organization API key, submit a job, and poll
for the result — the same models, trained assets, and credit pool your team
already uses in the app, now available over a clean REST interface and a remote
[MCP server](/mcp-server).
**Base URL** — all REST endpoints live under
```
https://api.samsa.ai/public/v1
```
## Start here
Create a key and generate your first image in five steps, with copy-paste
curl, Python, and TypeScript.
The base URL, authentication, and the conventions every endpoint shares.
Keys are organization-owned, scoped, and shown once. An org admin creates
them in your Samsa settings.
Connect Samsa to ChatGPT, Claude, or any MCP client over OAuth or an API key.
## What you can build
Turn a prompt into images, optionally composing your organization's trained
**style**, **object**, **person**, and **setting** models and color palettes.
Edit an existing image with a prompt — with or without a mask — and reuse the
same trained models for on-brand results.
Transform an image you already have — img2img, variations, resize by
outpainting, upscale, background removal, and vectorization to SVG.
Produce video from a start frame (with an optional end frame), from text, or
from text styled with your trained models.
Create custom models in four categories — style, object, person, and setting —
from a handful of reference images.
Introspect a key, read the credit balance it can spend, and review your
organization's API usage.
Every generation endpoint is **asynchronous**: a `POST` returns `202 Accepted`
with a job `id`, you poll the matching `GET` endpoint for status, and — once the
job is `completed` — the response carries presigned URLs to the finished assets.
You can also pass a `webhook_url` to be notified on completion instead of polling.
## Authentication
Send your key as a bearer token on every request:
```
Authorization: Bearer samsa_sk_your_key_here
```
Keys carry **scopes** (`images.generate`, `images.edit`, `images.transform`,
`videos.generate`, `models.read`, `models.write`, `usage.read`) and act for the
key's organization —
credits are drawn from that organization's pool and generated assets appear in the
app under the key creator's account. See the [quickstart](/quickstart) to create
your first key.
## Model Context Protocol (MCP)
Samsa also exposes a remote **MCP server** so agentic clients can generate,
edit, and transform media as tools:
```
https://api.samsa.ai/mcp
```
The MCP endpoint supports **dual authentication** — OAuth 2.1 (with a consent
screen in the Samsa app) for interactive clients such as ChatGPT and Claude, and
`Authorization: Bearer ` for headless clients such as scripts and n8n.
See the [MCP server guide](/mcp-server) for the tool catalog and per-client setup.
## Credits and pricing
API actions draw from your organization's existing Samsa credit pool at the same
rates as the app — for example, image generation costs `5` credits per output at
`1K`, scaling with resolution (`1K` ×1, `2K` ×2, `4K` ×4). When the pool is
exhausted, requests return `402`. Check what a key can spend any time with
[`GET /credits`](/api-reference/account/credits) — organizations that reserve credits
as per-team budgets get a per-key balance there, not just the organization total.
# Get credits
Source: https://docs.samsa.ai/api-reference/account/credits
api-reference/openapi.json GET /credits
Read the credit balance this API key can spend.
Returns what the **calling credential** — this API key or OAuth connection — can spend
right now, alongside the organization-wide totals it is derived from. Requires the
`usage.read` scope. For rates, see [Pricing](/guides/pricing).
## Credential-spendable vs organization-wide
The response carries two kinds of number. `available` and `spendable_plan_credits`
answer "what can *this* credential spend"; `plan_credits` and `topup_credits` are
organization-wide balances.
| Field | What it is |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `available` | Credits this credential can spend **right now** — `spendable_plan_credits + topup_credits`. This is the number to compare a job's `estimated_credits` against. |
| `spendable_plan_credits` | The plan-pool part of `available`: `plan_credits` capped by `scope.remaining`. Equal to `plan_credits` when nothing caps this credential. |
| `plan_credits` | The **organization's** remaining monthly plan pool — not necessarily all spendable by this credential. `0` when the pool is exhausted, or when the subscription is canceled and past its period end (top-ups stay spendable). |
| `topup_credits` | Remaining credits from valid, non-expired top-up purchases. Top-ups are exempt from team budgets, so they are always fully spendable. |
| `period` | The organization's current billing period. Team budget usage resets with it. |
| `scope` | The budget regime this credential spends under — see below. |
Three relations always hold:
* `available == spendable_plan_credits + topup_credits`
* `spendable_plan_credits <= plan_credits`
* for an organization with no active teams, `spendable_plan_credits == plan_credits`
**If your organization has no teams, nothing changed for you.** `scope.type` is
`org`, `spendable_plan_credits` equals `plan_credits`, and `available` is still the
plan pool plus top-ups.
## The budget regime (`scope`)
An organization can reserve parts of its monthly plan pool as per-team **budgets**, and
every API key can be assigned to a team. A credential then spends from exactly one
bucket, and only that bucket's headroom is available to it — which is why `available`
can be lower than the organization-wide `plan_credits`.
| `scope.type` | Meaning | Shortfall error code |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `org` | The organization has no active teams (the common case), or it is a budget-exempt system organization. Nothing caps this credential, so `budget`, `used`, and `remaining` are all `null`. | [`insufficient_credits`](/guides/errors#insufficient_credits) |
| `team` | The credential is bound to an active team that has its own monthly budget. | [`insufficient_team_credits`](/guides/errors#insufficient_team_credits) |
| `unallocated` | The organization has at least one active team, and this credential is **not** bound to a budgeted one — it has no team, or its team has no budget of its own. `budget` is the monthly plan allocation minus every active team budget, floored at `0`. | [`insufficient_unallocated_credits`](/guides/errors#insufficient_unallocated_credits) |
`scope.team` names the team the credential is bound to, or is `null` when it is bound to
none. `scope.remaining` is `budget - used`, floored at `0`.
A team-bound key whose budget is the binding constraint:
```json Response theme={null}
{
"available": 1450,
"spendable_plan_credits": 1200,
"plan_credits": 8031,
"topup_credits": 250,
"period": {
"start": "2026-07-01T00:00:00+00:00",
"end": "2026-08-01T00:00:00+00:00"
},
"scope": {
"type": "team",
"team": {
"id": "7f3c1e28-9b4d-4a61-8e52-0c7d6b5a4f39",
"name": "Marketing"
},
"budget": 10000,
"used": 8800,
"remaining": 1200
}
}
```
The organization still has `8031` plan credits, but the Marketing budget has `1200` left,
so this key can only spend `1200` of them — plus the `250` budget-exempt top-up credits,
for `1450` in total.
## Predicting and diagnosing a 402
**`available` is the predictor.** A job whose `estimated_credits` is at most `available`
is not refused with an insufficient-credit `402`, and conversely such a `402` means the
estimate exceeded `available` — on **either** number, the credential's, not the
organization's. One exception: an **operational failure inside the deduction itself**
(for example a degraded balance lookup) is reported as the generic
[`insufficient_credits`](/guides/errors#insufficient_credits) whatever the regime,
without proving anything about `available` — a plain `insufficient_credits` where you
expected a team-aware code can therefore be transient; retry before treating it as an
exhausted balance.
The prediction holds only as long as the credit and budget state does not change between
the read and the submit. Another credential of the same organization spending, a top-up
lot expiring, a subscription change, a team budget being edited, or a key being reassigned
to a different team can all move `available` — and even change `scope.type` and therefore
which error code you get — without any spend of your own.
[`subscription_inactive`](/guides/errors#subscription_inactive) is a separate `402`,
unrelated to this comparison.
When a submit is refused, `scope` explains it:
* **`scope.type` names the budget regime, which is what selects the error code**
(barring the operational-failure fallback above) — see the table above. It does
**not** tell you which balance ran out: a key under the `team`
regime gets `insufficient_team_credits` even when the organization's plan pool, and not
the team budget, is what was exhausted.
* To find the actual bottleneck under a capped regime (`team` or `unallocated`), compare
`plan_credits` with `scope.remaining` — the smaller one caps `spendable_plan_credits`.
Under `org`, `scope.remaining` is `null` and `plan_credits` is the only possible
bottleneck.
* `remaining: 0` means that bucket is fully spent for the current `period`. It is **not**
a precondition of the error: a bucket with `remaining: 100` and no top-ups still refuses
a job costing `200`.
So a key with no team, in an organization whose plan pool is fully allocated to teams,
reports `available: 0` with `scope.type: "unallocated"` and `remaining: 0` — every submit
that costs credits will be refused with `insufficient_unallocated_credits` until a budget is lowered, the
key is assigned to a team with headroom, or top-ups are purchased. See
[Errors](/guides/errors#insufficient_team_credits) for how to resolve each code.
# Get current key (me)
Source: https://docs.samsa.ai/api-reference/account/me
api-reference/openapi.json GET /me
Introspect the authenticated key, its organization, and available credits.
Returns `{ organization, api_key, credits }` for the presented key — the
organization id and name, the key's safe metadata (id, name, prefix, scopes,
expiry — never the secret), and the organization's available credit balance. This is
the fastest way to confirm a key works. `GET /me` requires **any** valid key and no
specific scope.
# Account & Usage
Source: https://docs.samsa.ai/api-reference/account/overview
Introspect the authenticated key, read the credit balance, and review API usage.
Inspect the authenticated key and your organization's credit position:
* [`GET /me`](/api-reference/account/me) — introspect the key, its organization, and
the available credit balance. Requires **any** valid key (no specific scope).
* [`GET /credits`](/api-reference/account/credits) — the balance **this key** can spend,
the organization-wide plan and top-up totals it comes from, and the budget regime the
key spends under. Requires `usage.read`.
* [`GET /usage`](/api-reference/account/usage) — API-attributed credit usage,
grouped by action or day. Requires `usage.read`.
`GET /me` is the fastest way to confirm a new key works and to check its scopes and
expiry before you wire it into an integration. See the [quickstart](/quickstart).
# Get usage
Source: https://docs.samsa.ai/api-reference/account/usage
api-reference/openapi.json GET /usage
Read the organization's API usage, grouped by action or day.
Aggregates your organization's API-attributed credit transactions into usage
buckets. Returns `total_credits`, `total_requests`, and a list of `buckets`
(`key`, `credits`, `requests`). Narrow the window with `from`/`to`, choose the
grouping with `group_by` (by action or by day), and scope to a single key with
`api_key_id`. Requires the `usage.read` scope.
# Edit an image
Source: https://docs.samsa.ai/api-reference/edits/create
api-reference/openapi.json POST /images/edits
Submit a Magic Edit job — a source image plus a prompt, optionally masked and styled.
Submit a source image and a prompt describing the edit. The call returns
**`202 Accepted`** with a job `id`; poll
[`GET /images/edits/{id}`](/api-reference/edits/get-edit) for the result. The
`image` object takes **exactly one** of `image_id`, `url`, or `base64` + `mime_type`.
## Example: masked edit from a URL
Provide the source by `url`, add an inpaint `mask` (its presence selects PRO mode),
and reuse a `style` model. The `mask` is a base64-encoded image marking the region
to edit.
```bash curl theme={null}
curl -X POST https://api.samsa.ai/public/v1/images/edits \
-H "Authorization: Bearer $SAMSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image": { "url": "https://cdn.example.com/product-photo.png" },
"prompt": "Replace the plain background with our branded gradient",
"mask": { "base64": "iVBORw0KGgoAAAANSUhEUgAA...." },
"style_id": "2b9d1f7a-3c4e-4a5b-9c8d-0e1f2a3b4c5d",
"engine": "nano_banana_pro",
"resolution": "1K",
"num_images": 1
}'
```
```json Body theme={null}
{
"image": { "url": "https://cdn.example.com/product-photo.png" },
"prompt": "Replace the plain background with our branded gradient",
"mask": { "base64": "iVBORw0KGgoAAAANSUhEUgAA...." },
"style_id": "2b9d1f7a-3c4e-4a5b-9c8d-0e1f2a3b4c5d",
"engine": "nano_banana_pro",
"resolution": "1K",
"num_images": 1
}
```
Send **exactly one** source mode in `image`. Omit `mask` for a text-only (SIMPLE)
edit. `resolution` applies only to `nano_banana_pro`; omit it for the other
engines. `num_images` (1–4) defaults to **1**.
# Get an image edit
Source: https://docs.samsa.ai/api-reference/edits/get-edit
api-reference/openapi.json GET /images/edits/{edit_id}
Poll a Magic Edit job for status and, once completed, the edited images.
Retrieve the status of a Magic Edit job. While it is `pending` or `processing`,
keep polling; once `completed`, the response carries the edited images, each with a
presigned `url` valid for **24 hours**. Only edit jobs your organization created are
visible; any other id returns [`404 not_found`](/guides/errors#not_found).
Pass a `webhook_url` on the original [edit request](/api-reference/edits/create) to
receive a signed callback instead of polling. See [Webhooks](/guides/webhooks).
# Image Editing
Source: https://docs.samsa.ai/api-reference/edits/overview
Edit an existing image with a prompt — with or without a mask — reusing your trained models.
Magic Edit takes a source image and a prompt and returns an edited image. Supply
the source three ways — an `image_id` already in your organization's context, an
`https` `url`, or inline `base64` + `mime_type`. Add a `mask` to run in **PRO**
(mask-based inpaint) mode, and reuse your trained `style`/`object`/`person`/
`setting` models and color palettes for on-brand results.
## Asynchronous pattern
Like generation, editing is asynchronous: `POST /images/edits` returns
**`202 Accepted`** with a job `id`; poll
[`GET /images/edits/{id}`](/api-reference/edits/get-edit) until `completed`, then
read the edited images (presigned URLs valid **24 hours**). A `webhook_url` gives
you a push callback instead — see [Webhooks](/guides/webhooks).
## Engines and credits
The default engine is `nano_banana_pro`; `gemini` and `kontext` are also available.
Supplying a `mask` runs the edit in **PRO** (mask-based) mode, supported by the
`nano_banana_pro` and `gemini` engines. Supplying trained models
(`style_id`/`object_ids`/`person_ids`/`setting_ids`) or a `color_palette_id` forces
`nano_banana_pro`. Each output is billed from your organization's pool at the app's
rates; an exhausted pool returns `402`. See [Pricing](/guides/pricing).
# Get a background-removal job
Source: https://docs.samsa.ai/api-reference/image-ops/get-background-removal
api-reference/openapi.json GET /images/background-removals/{background_removal_id}
Poll a background-removal job for status and, once completed, the transparent PNG.
Retrieve the status of a background-removal job. While it is `pending` or
`processing`, keep polling; once `completed`, the response carries a single `image`
result object — a transparent PNG with a presigned `url` valid for **24 hours**. A
cache-hit job is born `completed` with its result already attached. Only jobs your
organization created are visible; any other id returns
[`404 not_found`](/guides/errors#not_found). Requires the `images.transform` scope.
## Example: completed job
The `image` is a derived-format result object; for background removal `model`,
`width`, and `height` are always `null`. This example is a cache hit, so
`credits_used` is `0` (a normal miss shows `1`):
```json 200 OK theme={null}
{
"id": "f7a8b9c0-1d2e-4f3a-4b5c-6d7e8f9a0b1c",
"status": "completed",
"created_at": "2026-07-18T10:12:00Z",
"credits_used": 0,
"image": {
"id": "3c4d5e6f-7a8b-4c9d-0e1f-2a3b4c5d6e7f",
"source_image_id": "123e4567-e89b-12d3-a456-426614174000",
"format_type": "background_removal",
"mime_type": "image/png",
"url": "https://cdn.samsa.ai/user-.../no-bg.png?X-Amz-Signature=...",
"width": null,
"height": null,
"resolution_class": null,
"model": null
},
"error": null
}
```
Pass a `webhook_url` on the original
[background-removal request](/api-reference/image-ops/remove-background) to receive
a signed callback instead of polling — on a cache hit the `completed` event fires
immediately. See [Webhooks](/guides/webhooks).
# Get an img2img job
Source: https://docs.samsa.ai/api-reference/image-ops/get-img2img
api-reference/openapi.json GET /images/img2img/{img2img_id}
Poll an img2img job for status and, once completed, the produced images.
Retrieve the status of an img2img job. While it is `pending` or `processing`, keep
polling; once `completed`, the response carries the produced `images`, each with a
presigned `url` valid for **24 hours** — download the assets before they expire.
Only jobs your organization created are visible; any other id returns
[`404 not_found`](/guides/errors#not_found). Requires the `images.edit` scope.
## Example: completed job
```json 200 OK theme={null}
{
"id": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"status": "completed",
"created_at": "2026-07-18T10:12:00Z",
"credits_used": 5,
"images": [
{
"id": "9a8b7c6d-5e4f-4a3b-2c1d-0e1f2a3b4c5d",
"url": "https://cdn.samsa.ai/user-.../abc.png?X-Amz-Signature=...",
"thumbnail_url": "https://cdn.samsa.ai/user-.../abc-thumb.png?X-Amz-Signature=...",
"width": 1024,
"height": 1024,
"seed": 42
}
],
"error": null
}
```
Prefer push over polling? Pass a `webhook_url` on the original
[img2img request](/api-reference/image-ops/img2img) to receive a signed callback
when the job is `completed` or `failed`. A `cancelled` job emits no webhook — fall
back to polling for that case. See [Webhooks](/guides/webhooks).
# Get a resize job
Source: https://docs.samsa.ai/api-reference/image-ops/get-resize
api-reference/openapi.json GET /images/resizes/{resize_id}
Poll a resize job for status and, once completed, the produced images.
Retrieve the status of a resize job. While it is `pending` or `processing`, keep
polling; once `completed`, the response carries the produced `images`, each with a
presigned `url` valid for **24 hours**. Only jobs your organization created are
visible; any other id returns [`404 not_found`](/guides/errors#not_found). Requires
the `images.transform` scope.
## Example: completed job
```json 200 OK theme={null}
{
"id": "e6f7a8b9-0c1d-4e2f-3a4b-5c6d7e8f9a0b",
"status": "completed",
"created_at": "2026-07-18T10:12:00Z",
"credits_used": 5,
"images": [
{
"id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
"url": "https://cdn.samsa.ai/user-.../resized.png?X-Amz-Signature=...",
"thumbnail_url": null,
"width": 1024,
"height": 576,
"seed": null
}
],
"error": null
}
```
Pass a `webhook_url` on the original
[resize request](/api-reference/image-ops/resize) to receive a signed callback
instead of polling. See [Webhooks](/guides/webhooks).
# Get an upscale job
Source: https://docs.samsa.ai/api-reference/image-ops/get-upscale
api-reference/openapi.json GET /images/upscales/{upscale_id}
Poll an upscale job for status and, once completed, the produced image.
Retrieve the status of an upscale job. While it is `pending` or `processing`, keep
polling; once `completed`, the response carries a single `image` result object with
a presigned `url` valid for **24 hours**. Only jobs your organization created are
visible; any other id returns [`404 not_found`](/guides/errors#not_found). Requires
the `images.transform` scope.
## Example: completed job
Upscale returns one `image` — a derived-format result object (not the `images`
array used by generation), carrying its own stable `id`, the `source_image_id` it
derives from, the `model` that produced it, and a `resolution_class`:
```json 200 OK theme={null}
{
"id": "d5e6f7a8-9b0c-4d1e-2f3a-4b5c6d7e8f9a",
"status": "completed",
"created_at": "2026-07-18T10:12:00Z",
"credits_used": 10,
"image": {
"id": "7c8d9e0f-1a2b-4c3d-4e5f-6a7b8c9d0e1f",
"source_image_id": "123e4567-e89b-12d3-a456-426614174000",
"format_type": "upscale",
"mime_type": "image/png",
"url": "https://cdn.samsa.ai/user-.../upscaled.png?X-Amz-Signature=...",
"width": 3840,
"height": 2160,
"resolution_class": "4K",
"model": "crystal"
},
"error": null
}
```
Pass a `webhook_url` on the original
[upscale request](/api-reference/image-ops/upscale) to receive a signed callback
instead of polling. See [Webhooks](/guides/webhooks).
# Get a variations job
Source: https://docs.samsa.ai/api-reference/image-ops/get-variations
api-reference/openapi.json GET /images/variations/{variation_id}
Poll a variations job for status and, once completed, the produced images.
Retrieve the status of a variations job. While it is `pending` or `processing`, keep
polling; once `completed`, the response carries the produced `images`, each with a
presigned `url` valid for **24 hours**. Only jobs your organization created are
visible; any other id returns [`404 not_found`](/guides/errors#not_found). Requires
the `images.transform` scope.
## Example: completed job
```json 200 OK theme={null}
{
"id": "c4d5e6f7-8a9b-4c1d-2e3f-4a5b6c7d8e9f",
"status": "completed",
"created_at": "2026-07-18T10:12:00Z",
"credits_used": 5,
"images": [
{
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"url": "https://cdn.samsa.ai/user-.../var.png?X-Amz-Signature=...",
"thumbnail_url": null,
"width": 1024,
"height": 1024,
"seed": null
}
],
"error": null
}
```
Pass a `webhook_url` on the original
[variations request](/api-reference/image-ops/variations) to receive a signed
callback instead of polling. See [Webhooks](/guides/webhooks).
# Get a vectorization job
Source: https://docs.samsa.ai/api-reference/image-ops/get-vectorization
api-reference/openapi.json GET /images/vectorizations/{vectorization_id}
Poll a vectorization job for status and, once completed, the produced SVG.
Retrieve the status of a vectorization job. While it is `pending` or `processing`,
keep polling; once `completed`, the response carries a single `svg` result object
(`image/svg+xml`) with a presigned `url` valid for **24 hours**. A cache-hit job is
born `completed` with its result already attached. Only jobs your organization
created are visible; any other id returns
[`404 not_found`](/guides/errors#not_found). Requires the `images.transform` scope.
The delivered SVG is an EU AI Act Art. 50(2) scope-out — **unsigned and
unwatermarked**. Delivery is revalidated against the server-verified ToS/AUP
acceptance on read; a missing or stale acceptance returns
`403 svg_phase1_scope_out_required`.
## Example: completed job
The `svg` is a vectorization result object; its `mime_type` is always
`image/svg+xml`:
```json 200 OK theme={null}
{
"id": "a8b9c0d1-2e3f-4a4b-5c6d-7e8f9a0b1c2d",
"status": "completed",
"created_at": "2026-07-18T10:12:00Z",
"credits_used": 5,
"svg": {
"id": "4d5e6f7a-8b9c-4d0e-1f2a-3b4c5d6e7f8a",
"source_image_id": "123e4567-e89b-12d3-a456-426614174000",
"mime_type": "image/svg+xml",
"url": "https://cdn.samsa.ai/user-.../vector.svg?X-Amz-Signature=..."
},
"error": null
}
```
Pass a `webhook_url` on the original
[vectorization request](/api-reference/image-ops/vectorize) to receive a signed
callback instead of polling — on a cache hit the `completed` event fires
immediately. See [Webhooks](/guides/webhooks).
# Transform images (img2img)
Source: https://docs.samsa.ai/api-reference/image-ops/img2img
api-reference/openapi.json POST /images/img2img
Submit an img2img job — 1–14 reference images plus a prompt — and get a job id back.
Transform 1–14 source `images` with a `prompt`. The call returns **`202 Accepted`**
with a job `id`; poll [`GET /images/img2img/{id}`](/api-reference/image-ops/get-img2img)
for the result. Each entry in `images` is a source object taking **exactly one** of
`image_id`, `url`, or `base64` + `mime_type`; order is preserved. Requires the
`images.edit` scope.
## Example: one source per mode
```bash image_id theme={null}
curl -X POST https://api.samsa.ai/public/v1/images/img2img \
-H "Authorization: Bearer $SAMSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"images": [{ "image_id": "123e4567-e89b-12d3-a456-426614174000" }],
"prompt": "Place the product on a marble kitchen counter"
}'
```
```bash url theme={null}
curl -X POST https://api.samsa.ai/public/v1/images/img2img \
-H "Authorization: Bearer $SAMSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"images": [{ "url": "https://cdn.example.com/photo.png" }],
"prompt": "Turn this sketch into a photorealistic render",
"resolution": "2K"
}'
```
```bash base64 theme={null}
curl -X POST https://api.samsa.ai/public/v1/images/img2img \
-H "Authorization: Bearer $SAMSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"images": [{ "base64": "iVBORw0KGgoAAAANSUhEUg...", "mime_type": "image/png" }],
"prompt": "Apply a warm sunset color grade",
"num_outputs": 2
}'
```
## Example: blend multiple sources
Pass several sources — mixing modes is allowed — and tune the engine, aspect ratio,
and resolution:
```json Body theme={null}
{
"images": [
{ "image_id": "123e4567-e89b-12d3-a456-426614174000" },
{ "url": "https://cdn.example.com/reference.jpg" },
{ "base64": "iVBORw0KGgoAAAANSUhEUg...", "mime_type": "image/webp" }
],
"prompt": "Blend the subject into the reference scene",
"engine": "nano_banana_2",
"aspect_ratio": "16:9",
"resolution": "4K",
"webhook_url": "https://example.com/webhooks/samsa"
}
```
The `202` response is the async job handle:
```json 202 Accepted theme={null}
{
"id": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"status": "pending",
"estimated_credits": 20
}
```
`images` holds 1–14 sources; each is **exactly one** of `image_id`, an https
`url`, or `base64` + `mime_type`. `prompt` is required. `engine` is
`nano_banana_pro` (default) or `nano_banana_2`; an unknown engine returns `422`.
`aspect_ratio` is validated against the engine's supported list (`1:1`, `2:3`,
`3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `16:9`, `21:9`; `nano_banana_2` also
`4:1`, `1:4`, `8:1`, `1:8`); omitted preserves the source shape. `resolution` is
`1K` (default), `2K`, or `4K`. `num_outputs` (1–4) defaults to **1** and each
output is billed. `output_format` is `png` only in v1.
## Credits
Each output costs `5` credits at `1K`, scaling with resolution (`1K` ×1, `2K` ×2,
`4K` ×4) and multiplied by `num_outputs`. See [Pricing](/guides/pricing).
## Errors
| Status | Code | When |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `402` | [`insufficient_credits`](/guides/errors#insufficient_credits) / [`insufficient_team_credits`](/guides/errors#insufficient_team_credits) / [`insufficient_unallocated_credits`](/guides/errors#insufficient_unallocated_credits) | The balance this credential can spend is below the job cost. Organizations with at least one active team get the team-aware codes instead of `insufficient_credits` (budget-exempt system organizations excepted). An operational failure inside the deduction can still surface as the generic `insufficient_credits`, whatever the regime. |
| `402` | [`subscription_inactive`](/guides/errors#subscription_inactive) | The organization has no usable subscription. |
| `403` | [`missing_scope`](/guides/errors#missing_scope) | Key lacks the `images.edit` scope. |
| `404` | [`not_found`](/guides/errors#not_found) | A source `image_id` is unknown, or not owned by the API key's creator — an image another member of your organization created is a `404` too. |
| `422` | [`validation_error`](/guides/errors#validation_error) | Zero/multiple source modes, bad prompt, unknown engine, or unsupported aspect ratio. |
| `429` | [`rate_limited`](/guides/errors#rate_limited) / [`too_many_active_jobs`](/guides/errors#too_many_active_jobs) | Per-key rate window or per-org concurrency cap exceeded. |
# Image Operations
Source: https://docs.samsa.ai/api-reference/image-ops/overview
Transform an existing image — img2img, variations, upscale, resize, background removal, and vectorization.
The image-operation endpoints transform an image you already have — no prompt-only
generation. Six operations, each a `POST` that submits a job plus a `GET` that polls
it:
| Operation | Submit | What it does | Scope |
| ---------------------- | -------------------------------------------------------------------------------- | --------------------------------------------------- | ------------------ |
| **Img2img** | [`POST /images/img2img`](/api-reference/image-ops/img2img) | Transform 1–14 reference images with a prompt | `images.edit` |
| **Variations** | [`POST /images/variations`](/api-reference/image-ops/variations) | Generate creative variations of one image | `images.transform` |
| **Upscale** | [`POST /images/upscales`](/api-reference/image-ops/upscale) | Upscale one image to a higher resolution | `images.transform` |
| **Resize** | [`POST /images/resizes`](/api-reference/image-ops/resize) | Resize to a new aspect ratio (server-side outpaint) | `images.transform` |
| **Background removal** | [`POST /images/background-removals`](/api-reference/image-ops/remove-background) | Remove the background (transparent PNG) | `images.transform` |
| **Vectorization** | [`POST /images/vectorizations`](/api-reference/image-ops/vectorize) | Vectorize one image to SVG | `images.transform` |
## Source image
Every operation takes its source the same three ways — supply **exactly one** mode
per source:
* `image_id` — the id of an image the **API key's creator** owns in Samsa; any other
id, including the id of an image another member of your organization created,
returns `404`.
* `url` — an `https` URL the server downloads under its SSRF guard.
* `base64` + `mime_type` — inline bytes, `mime_type` one of `image/jpeg`,
`image/png`, `image/webp`.
Zero or more than one mode is a `422`. Img2img accepts an `images` **array** (1–14
sources); the other five take a single `image` object.
## Asynchronous pattern
Every submit returns **`202 Accepted`** with a job handle
`{ "id", "status": "pending", "estimated_credits" }`. Poll the operation's
`GET .../{id}` endpoint until `status` is `completed` (or `failed`/`cancelled`),
then read the result — each produced asset carries a presigned `url` valid for
**24 hours**. Pass a `webhook_url` to be notified instead of polling (see
[Webhooks](/guides/webhooks)).
**Cache hits (background removal & vectorization).** When the source is an
`image_id` you own and a result already exists for it, the submit returns `202`
with `status: "completed"` and `estimated_credits: 0` **immediately** — no new job
is queued and your organization's concurrency cap is not consumed. Every other
source (an `https` `url`, `base64`, or an owned `image_id` with no ready result)
is the normal charged, asynchronous path.
## Scopes
Img2img is an **edit** operation and requires the `images.edit` scope. The other
five are **transform** operations and require `images.transform`. A key missing the
required scope receives [`403 missing_scope`](/guides/errors#missing_scope).
## Credits
Costs draw from the balance the calling credential can spend, not necessarily the
organization's whole pool; too little returns
[`402 insufficient_credits`](/guides/errors#insufficient_credits), or the team-aware
[`insufficient_team_credits`](/guides/errors#insufficient_team_credits) /
[`insufficient_unallocated_credits`](/guides/errors#insufficient_unallocated_credits)
in organizations with at least one active team (budget-exempt system organizations
stay on the generic code). An operational failure inside the deduction itself can
also surface as the generic `insufficient_credits`, whatever the regime. Each operation's page states its cost
basis. See [Pricing](/guides/pricing) and
[`GET /credits`](/api-reference/account/credits).
# Remove a background
Source: https://docs.samsa.ai/api-reference/image-ops/remove-background
api-reference/openapi.json POST /images/background-removals
Submit a background-removal job — one source image — and get a transparent PNG back.
Remove the background from one source `image`, producing a transparent PNG. The call
returns **`202 Accepted`** with a job `id`; poll
[`GET /images/background-removals/{id}`](/api-reference/image-ops/get-background-removal)
for the result. The `image` takes **exactly one** of `image_id`, `url`, or
`base64` + `mime_type`. There is **no model parameter** — v1 uses the default
background-removal model. Requires the `images.transform` scope.
## Example: one source per mode
```bash image_id theme={null}
curl -X POST https://api.samsa.ai/public/v1/images/background-removals \
-H "Authorization: Bearer $SAMSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "image": { "image_id": "123e4567-e89b-12d3-a456-426614174000" } }'
```
```bash url theme={null}
curl -X POST https://api.samsa.ai/public/v1/images/background-removals \
-H "Authorization: Bearer $SAMSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image": { "url": "https://cdn.example.com/photo.png" },
"webhook_url": "https://example.com/webhooks/samsa"
}'
```
```bash base64 theme={null}
curl -X POST https://api.samsa.ai/public/v1/images/background-removals \
-H "Authorization: Bearer $SAMSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "image": { "base64": "iVBORw0KGgoAAAANSUhEUg...", "mime_type": "image/png" } }'
```
The normal (charged) path returns a `pending` job:
```json 202 Accepted theme={null}
{
"id": "f7a8b9c0-1d2e-4f3a-4b5c-6d7e8f9a0b1c",
"status": "pending",
"estimated_credits": 1
}
```
**Cost = 1 credit on a miss; 0 on a cache hit.** When the source is an `image_id`
you own and a background-removed result already exists for it, the submit returns
`202` with `status: "completed"` and `estimated_credits: 0` **immediately** — no
new job is queued and your organization's concurrency cap is not consumed. Every
other case (an `https` `url`/`base64` source, or an owned `image_id` with no ready
result) is the normal charged path above.
```json 202 Accepted (cache hit) theme={null}
{
"id": "f7a8b9c0-1d2e-4f3a-4b5c-6d7e8f9a0b1c",
"status": "completed",
"estimated_credits": 0
}
```
## Errors
| Status | Code | When |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `402` | [`insufficient_credits`](/guides/errors#insufficient_credits) / [`insufficient_team_credits`](/guides/errors#insufficient_team_credits) / [`insufficient_unallocated_credits`](/guides/errors#insufficient_unallocated_credits) | The balance this credential can spend is below the job cost. Organizations with at least one active team get the team-aware codes instead of `insufficient_credits` (budget-exempt system organizations excepted). An operational failure inside the deduction can still surface as the generic `insufficient_credits`, whatever the regime. |
| `402` | [`subscription_inactive`](/guides/errors#subscription_inactive) | The organization has no usable subscription. |
| `403` | [`missing_scope`](/guides/errors#missing_scope) | Key lacks the `images.transform` scope. |
| `404` | [`not_found`](/guides/errors#not_found) | The source `image_id` is unknown, or not owned by the API key's creator — an image another member of your organization created is a `404` too. |
| `422` | [`validation_error`](/guides/errors#validation_error) | Zero or multiple source modes supplied. |
| `429` | [`rate_limited`](/guides/errors#rate_limited) / [`too_many_active_jobs`](/guides/errors#too_many_active_jobs) | Per-key rate window or per-org concurrency cap exceeded. |
# Resize an image
Source: https://docs.samsa.ai/api-reference/image-ops/resize
api-reference/openapi.json POST /images/resizes
Submit a resize job — one source image to a new aspect ratio (server-side outpaint).
Resize one source `image` to a new `aspect_ratio`. Resize is a **server-side
outpaint**: the backend renders the composition and mask from your source at the
target ratio and fills the newly exposed canvas with a style-matched extension. The
call returns **`202 Accepted`** with a job `id`; poll
[`GET /images/resizes/{id}`](/api-reference/image-ops/get-resize) for the result.
The `image` takes **exactly one** of `image_id`, `url`, or `base64` + `mime_type`.
Requires the `images.transform` scope.
## Example: one source per mode
```bash image_id theme={null}
curl -X POST https://api.samsa.ai/public/v1/images/resizes \
-H "Authorization: Bearer $SAMSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image": { "image_id": "123e4567-e89b-12d3-a456-426614174000" },
"aspect_ratio": "16:9"
}'
```
```bash url theme={null}
curl -X POST https://api.samsa.ai/public/v1/images/resizes \
-H "Authorization: Bearer $SAMSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image": { "url": "https://cdn.example.com/portrait.png" },
"aspect_ratio": "1:1",
"resolution": "2K",
"placement": { "gravity": "top", "scale": 0.8 }
}'
```
```bash base64 theme={null}
curl -X POST https://api.samsa.ai/public/v1/images/resizes \
-H "Authorization: Bearer $SAMSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image": { "base64": "iVBORw0KGgoAAAANSUhEUg...", "mime_type": "image/png" },
"aspect_ratio": "21:9",
"num_outputs": 2,
"prompt": "extend the mountain range on both sides",
"webhook_url": "https://example.com/webhooks/samsa"
}'
```
The `202` response is the async job handle:
```json 202 Accepted theme={null}
{
"id": "e6f7a8b9-0c1d-4e2f-3a4b-5c6d7e8f9a0b",
"status": "pending",
"estimated_credits": 5
}
```
`aspect_ratio` is **required** and one of `21:9`, `16:9`, `3:2`, `4:3`, `5:4`,
`1:1`, `4:5`, `3:4`, `2:3`, `9:16`. `resolution` (`1K` default, `2K`, `4K`) is
provider metadata that scales credits — the canvas is always rendered at a 1024px
max edge. `num_outputs` (1–4) defaults to **1** and each output is billed.
`prompt` is optional guidance for the newly generated area (validated when
non-empty). `placement` positions the source on the target canvas: `gravity` is
one of `center` (default), `top`, `bottom`, `left`, `right`, `top_left`,
`top_right`, `bottom_left`, `bottom_right`; `scale` is in `(0, 1]` (default `1.0`
\= maximum contain-fit).
**Empty-mask no-op.** When the requested `aspect_ratio` + `placement` leave
(almost) no new area to generate — e.g. the source already matches the target
ratio at `scale = 1` — and no `prompt` is given, the job skips the model, returns
the flattened composition, and **refunds the unused outputs**. It still
`completes`.
## Credits
Each output costs `5` credits at `1K`, scaling with resolution (`1K` ×1, `2K` ×2,
`4K` ×4) and multiplied by `num_outputs`. See [Pricing](/guides/pricing).
## Errors
| Status | Code | When |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `402` | [`insufficient_credits`](/guides/errors#insufficient_credits) / [`insufficient_team_credits`](/guides/errors#insufficient_team_credits) / [`insufficient_unallocated_credits`](/guides/errors#insufficient_unallocated_credits) | The balance this credential can spend is below the job cost. Organizations with at least one active team get the team-aware codes instead of `insufficient_credits` (budget-exempt system organizations excepted). An operational failure inside the deduction can still surface as the generic `insufficient_credits`, whatever the regime. |
| `402` | [`subscription_inactive`](/guides/errors#subscription_inactive) | The organization has no usable subscription. |
| `403` | [`missing_scope`](/guides/errors#missing_scope) | Key lacks the `images.transform` scope. |
| `404` | [`not_found`](/guides/errors#not_found) | The source `image_id` is unknown, or not owned by the API key's creator — an image another member of your organization created is a `404` too. |
| `422` | [`validation_error`](/guides/errors#validation_error) | Zero/multiple source modes, missing/invalid `aspect_ratio`, bad `placement`, a policy-violating `prompt`, or a source raster above **33,554,432 pixels** (32 MP, width × height), which is rejected with `param: image` before any rendering. |
| `429` | [`rate_limited`](/guides/errors#rate_limited) / [`too_many_active_jobs`](/guides/errors#too_many_active_jobs) | Per-key rate window or per-org concurrency cap exceeded. |
# Upscale an image
Source: https://docs.samsa.ai/api-reference/image-ops/upscale
api-reference/openapi.json POST /images/upscales
Submit an upscale job — one source image to a higher target resolution — and get a job id back.
Upscale one source `image` to a higher `target_resolution` with one of four models.
The call returns **`202 Accepted`** with a job `id`; poll
[`GET /images/upscales/{id}`](/api-reference/image-ops/get-upscale) for the result.
The `image` takes **exactly one** of `image_id`, `url`, or `base64` + `mime_type`.
Requires the `images.transform` scope.
## Example: one source per mode
```bash image_id theme={null}
curl -X POST https://api.samsa.ai/public/v1/images/upscales \
-H "Authorization: Bearer $SAMSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image": { "image_id": "123e4567-e89b-12d3-a456-426614174000" },
"model": "crystal",
"target_resolution": "4K"
}'
```
```bash url theme={null}
curl -X POST https://api.samsa.ai/public/v1/images/upscales \
-H "Authorization: Bearer $SAMSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image": { "url": "https://cdn.example.com/photo.png" },
"model": "seedvr",
"target_resolution": "2K"
}'
```
```bash base64 theme={null}
curl -X POST https://api.samsa.ai/public/v1/images/upscales \
-H "Authorization: Bearer $SAMSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image": { "base64": "iVBORw0KGgoAAAANSUhEUg...", "mime_type": "image/png" },
"model": "magnific-precision",
"target_resolution": "6K",
"options": { "magnific_precision": { "flavor": "photo", "sharpen": 20, "ultra_detail": 40 } }
}'
```
## Example: magnific-creative with options
```json Body theme={null}
{
"image": { "image_id": "123e4567-e89b-12d3-a456-426614174000" },
"model": "magnific-creative",
"target_resolution": "8K",
"options": {
"magnific_creative": {
"prompt": "sharp studio product photo",
"optimized_for": "films_n_photography",
"creativity": 2,
"hdr": 1,
"engine": "magnific_sharpy"
}
},
"webhook_url": "https://example.com/webhooks/samsa"
}
```
The `202` response is the async job handle:
```json 202 Accepted theme={null}
{
"id": "d5e6f7a8-9b0c-4d1e-2f3a-4b5c6d7e8f9a",
"status": "pending",
"estimated_credits": 75
}
```
`model` is `crystal` (default), `seedvr`, `magnific-creative`, or
`magnific-precision`. `target_resolution` is **required** and drawn from a closed
set: `2K`, `4K`, `6K`, `8K`, `10K`, `12K`, `14K`, `16K`, `20K`, `24K`, `28K`,
`32K`, `38K`. Classes above `16K` are valid for `crystal` only. The per-model
factor and area caps are validated **before** any credits are charged — an
over-cap request is a `422`, never a charged job.
`options` are **per-model, typed, and strictly validated**: supply
`magnific_creative` for the `magnific-creative` model or `magnific_precision` for
`magnific-precision`. `seedvr` and `crystal` take no options — supplying `options`
for them is a `422`. Unknown keys or out-of-range values are `422`.
* `magnific_creative`: `prompt` (≤ 500 chars), `optimized_for`
(`standard` · `soft_portraits` · `hard_portraits` · `art_n_illustration` ·
`videogame_assets` · `nature_n_landscapes` · `films_n_photography` ·
`3d_renders` · `science_fiction_n_horror`), `creativity`/`hdr`/`resemblance`/
`fractality` (integers −10…10), `engine` (`automatic` · `magnific_illusio` ·
`magnific_sharpy` · `magnific_sparkle`).
* `magnific_precision`: `sharpen`/`smart_grain`/`ultra_detail` (integers 0…100),
`flavor` (`sublime` (default) · `photo` · `photo_denoiser`).
## Credits
Cost is the resolution tier (`2K`:5, `4K`:10, `6K`:20, `8K`:25, `10K`:35, `12K`:45,
`14K`:60, `16K`:80) multiplied by the model credit multiplier (Magnific ×3); Crystal
above `12K` prices by output megapixels. The `estimated_credits` in the `202`
response equals the deduction exactly. See [Pricing](/guides/pricing).
## Errors
| Status | Code | When |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `402` | [`insufficient_credits`](/guides/errors#insufficient_credits) / [`insufficient_team_credits`](/guides/errors#insufficient_team_credits) / [`insufficient_unallocated_credits`](/guides/errors#insufficient_unallocated_credits) | The balance this credential can spend is below the job cost. Organizations with at least one active team get the team-aware codes instead of `insufficient_credits` (budget-exempt system organizations excepted). An operational failure inside the deduction can still surface as the generic `insufficient_credits`, whatever the regime. |
| `402` | [`subscription_inactive`](/guides/errors#subscription_inactive) | The organization has no usable subscription. |
| `403` | [`missing_scope`](/guides/errors#missing_scope) | Key lacks the `images.transform` scope. |
| `404` | [`not_found`](/guides/errors#not_found) | The source `image_id` is unknown, or not owned by the API key's creator — an image another member of your organization created is a `404` too. |
| `422` | [`validation_error`](/guides/errors#validation_error) | Zero/multiple source modes, missing/invalid `target_resolution`, an over-cap class, or invalid/misplaced `options`. |
| `429` | [`rate_limited`](/guides/errors#rate_limited) / [`too_many_active_jobs`](/guides/errors#too_many_active_jobs) | Per-key rate window or per-org concurrency cap exceeded. |
# Generate variations
Source: https://docs.samsa.ai/api-reference/image-ops/variations
api-reference/openapi.json POST /images/variations
Submit a variations job — one source image, optionally guided — and get a job id back.
Generate creative variations of one source `image`. The call returns
**`202 Accepted`** with a job `id`; poll
[`GET /images/variations/{id}`](/api-reference/image-ops/get-variations) for the
result. The `image` takes **exactly one** of `image_id`, `url`, or
`base64` + `mime_type`. Requires the `images.transform` scope.
## Example: one source per mode
```bash image_id theme={null}
curl -X POST https://api.samsa.ai/public/v1/images/variations \
-H "Authorization: Bearer $SAMSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "image": { "image_id": "123e4567-e89b-12d3-a456-426614174000" } }'
```
```bash url theme={null}
curl -X POST https://api.samsa.ai/public/v1/images/variations \
-H "Authorization: Bearer $SAMSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image": { "url": "https://cdn.example.com/photo.png" },
"target": "object",
"creativity": "subtle",
"num_outputs": 4,
"variation_instructions": "Try different background colors"
}'
```
```bash base64 theme={null}
curl -X POST https://api.samsa.ai/public/v1/images/variations \
-H "Authorization: Bearer $SAMSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image": { "base64": "iVBORw0KGgoAAAANSUhEUg...", "mime_type": "image/png" },
"preservation_instructions": "Keep the product shape unchanged",
"webhook_url": "https://example.com/webhooks/samsa"
}'
```
The `202` response is the async job handle:
```json 202 Accepted theme={null}
{
"id": "c4d5e6f7-8a9b-4c1d-2e3f-4a5b6c7d8e9f",
"status": "pending",
"estimated_credits": 5
}
```
Send **exactly one** source mode in `image`. `target` — what the variations may
change — is `everything` (default), `person`, `object`, or `scene`. `creativity`
is `subtle` or `creative` (default). `variation_instructions` (what to change) and
`preservation_instructions` (what to keep) are optional free text, ≤ 2000
characters each. `num_outputs` (1–4) defaults to **1** and each output is billed.
There is no `resolution` parameter — the output resolution is **inherited from the
source**.
## Credits
Each output costs `5` credits at `1K`, scaling with the **source image's**
resolution tier (`1K` ×1, `2K` ×2, `4K` ×4) and multiplied by `num_outputs`. See
[Pricing](/guides/pricing).
## Errors
| Status | Code | When |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `402` | [`insufficient_credits`](/guides/errors#insufficient_credits) / [`insufficient_team_credits`](/guides/errors#insufficient_team_credits) / [`insufficient_unallocated_credits`](/guides/errors#insufficient_unallocated_credits) | The balance this credential can spend is below the job cost. Organizations with at least one active team get the team-aware codes instead of `insufficient_credits` (budget-exempt system organizations excepted). An operational failure inside the deduction can still surface as the generic `insufficient_credits`, whatever the regime. |
| `402` | [`subscription_inactive`](/guides/errors#subscription_inactive) | The organization has no usable subscription. |
| `403` | [`missing_scope`](/guides/errors#missing_scope) | Key lacks the `images.transform` scope. |
| `404` | [`not_found`](/guides/errors#not_found) | The source `image_id` is unknown, or not owned by the API key's creator — an image another member of your organization created is a `404` too. |
| `422` | [`validation_error`](/guides/errors#validation_error) | Zero/multiple source modes, bad `target`/`creativity`, or over-long instructions. |
| `429` | [`rate_limited`](/guides/errors#rate_limited) / [`too_many_active_jobs`](/guides/errors#too_many_active_jobs) | Per-key rate window or per-org concurrency cap exceeded. |
# Vectorize an image
Source: https://docs.samsa.ai/api-reference/image-ops/vectorize
api-reference/openapi.json POST /images/vectorizations
Submit a vectorization job — one source image to SVG — and get a job id back.
Vectorize one source `image` into an SVG. The call returns **`202 Accepted`** with a
job `id`; poll [`GET /images/vectorizations/{id}`](/api-reference/image-ops/get-vectorization)
for the result. The `image` takes **exactly one** of `image_id`, `url`, or
`base64` + `mime_type`. There is **no model parameter**. Requires the
`images.transform` scope.
**SVG is an EU AI Act Art. 50(2) scope-out.** An SVG cannot carry a C2PA manifest
or an embedded watermark, so vector outputs are delivered **unsigned and
unwatermarked**. Delivery is gated on `svg_acceptance` — an explicit acknowledgment
of this. **The acknowledgment is disclosure / audit evidence, not a compliance
waiver.**
## Example: one source per mode
`svg_acceptance` must be the literal boolean `true` on every request:
```bash image_id theme={null}
curl -X POST https://api.samsa.ai/public/v1/images/vectorizations \
-H "Authorization: Bearer $SAMSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image": { "image_id": "123e4567-e89b-12d3-a456-426614174000" },
"svg_acceptance": true
}'
```
```bash url theme={null}
curl -X POST https://api.samsa.ai/public/v1/images/vectorizations \
-H "Authorization: Bearer $SAMSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image": { "url": "https://cdn.example.com/logo.png" },
"svg_acceptance": true,
"webhook_url": "https://example.com/webhooks/samsa"
}'
```
```bash base64 theme={null}
curl -X POST https://api.samsa.ai/public/v1/images/vectorizations \
-H "Authorization: Bearer $SAMSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image": { "base64": "iVBORw0KGgoAAAANSUhEUg...", "mime_type": "image/png" },
"svg_acceptance": true
}'
```
The normal (charged) path returns a `pending` job:
```json 202 Accepted theme={null}
{
"id": "a8b9c0d1-2e3f-4a4b-5c6d-7e8f9a0b1c2d",
"status": "pending",
"estimated_credits": 5
}
```
**Cost = 5 credits on a miss; 0 on a cache hit.** When the source is an `image_id`
you own and a vector result already exists for it, the submit returns `202` with
`status: "completed"` and `estimated_credits: 0` immediately — no new job is queued
and your organization's concurrency cap is not consumed.
## Acceptance gates
Vectorization has two independent delivery gates, both enforced with **no charge** on
failure:
* `svg_acceptance` must be the literal boolean `true`. A missing, `false`, or any
other value is rejected `422 svg_acceptance_required`.
* A **current, server-verified ToS/AUP acceptance** is also required — the request
flag is never trusted as this fact. A missing or stale acceptance is rejected
`403 svg_phase1_scope_out_required`; accept the current ToS/AUP and retry.
## Errors
| Status | Code | When |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `402` | [`insufficient_credits`](/guides/errors#insufficient_credits) / [`insufficient_team_credits`](/guides/errors#insufficient_team_credits) / [`insufficient_unallocated_credits`](/guides/errors#insufficient_unallocated_credits) | The balance this credential can spend is below the job cost. Organizations with at least one active team get the team-aware codes instead of `insufficient_credits` (budget-exempt system organizations excepted). An operational failure inside the deduction can still surface as the generic `insufficient_credits`, whatever the regime. |
| `402` | [`subscription_inactive`](/guides/errors#subscription_inactive) | The organization has no usable subscription. |
| `403` | [`missing_scope`](/guides/errors#missing_scope) | Key lacks the `images.transform` scope. |
| `403` | [`svg_phase1_scope_out_required`](/guides/errors#svg_phase1_scope_out_required) | The server-verified ToS/AUP scope-out acceptance is missing or stale (no charge). |
| `404` | [`not_found`](/guides/errors#not_found) | The source `image_id` is unknown, or not owned by the API key's creator — an image another member of your organization created is a `404` too. |
| `422` | [`validation_error`](/guides/errors#validation_error) | Zero or multiple source modes supplied. |
| `422` | [`svg_acceptance_required`](/guides/errors#svg_acceptance_required) | `svg_acceptance` is not the literal boolean `true` (no charge). |
| `429` | [`rate_limited`](/guides/errors#rate_limited) / [`too_many_active_jobs`](/guides/errors#too_many_active_jobs) | Per-key rate window or per-org concurrency cap exceeded. |
# Generate images
Source: https://docs.samsa.ai/api-reference/images/generate
api-reference/openapi.json POST /images/generations
Submit an image-generation job — prompt plus optional trained models — and get a job id back.
Submit a prompt to generate one to four images. The call returns **`202 Accepted`**
with a job `id`; poll [`GET /images/generations/{id}`](/api-reference/images/get-generation)
for the result. Combine your organization's trained models by passing their ids or
names — `style_id`, `object_ids`, `person_ids`, `setting_ids` — plus a
`color_palette_id`. Each ref accepts a name or id that resolves to a model or
palette visible to you.
## Example: compose multiple trained models
Pass a `style` model, one or more `person` and `setting` models, and a color
palette together in one request. Every model reference — by id or name — must
resolve to a `completed` model visible to you.
```bash curl theme={null}
curl -X POST https://api.samsa.ai/public/v1/images/generations \
-H "Authorization: Bearer $SAMSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Our founder presenting on stage at the product launch, cinematic lighting",
"style_id": "2b9d1f7a-3c4e-4a5b-9c8d-0e1f2a3b4c5d",
"person_ids": ["c1a2b3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"],
"setting_ids": ["a9b8c7d6-e5f4-4a3b-2c1d-0e1f2a3b4c5d"],
"color_palette_id": "7f6e5d4c-3b2a-4c1d-9e8f-0a1b2c3d4e5f",
"engine": "nano_banana_pro",
"aspect_ratio": "16:9",
"resolution": "2K",
"num_outputs": 2
}'
```
```json Body theme={null}
{
"prompt": "Our founder presenting on stage at the product launch, cinematic lighting",
"style_id": "2b9d1f7a-3c4e-4a5b-9c8d-0e1f2a3b4c5d",
"person_ids": ["c1a2b3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"],
"setting_ids": ["a9b8c7d6-e5f4-4a3b-2c1d-0e1f2a3b4c5d"],
"color_palette_id": "7f6e5d4c-3b2a-4c1d-9e8f-0a1b2c3d4e5f",
"engine": "nano_banana_pro",
"aspect_ratio": "16:9",
"resolution": "2K",
"num_outputs": 2
}
```
`object_ids`, `person_ids`, and `setting_ids` are arrays; `style_id` and
`color_palette_id` take a single value (a name or id). `num_outputs` (1–4)
defaults to **1** and each output is billed. Omit `engine` to use the default
`nano_banana_pro`. `aspect_ratio` accepts `1:1` (default), `2:3`, `3:2`, `3:4`,
`4:3`, `4:5`, `5:4`, `9:16`, `16:9`, `21:9`, `1:4`, `4:1`, `1:8`, and `8:1`; an
unsupported ratio returns `422` before any credits are charged.
# Get an image generation
Source: https://docs.samsa.ai/api-reference/images/get-generation
api-reference/openapi.json GET /images/generations/{generation_id}
Poll an image-generation job for status and, once completed, the produced images.
Retrieve the status of a generation job. While it is `pending` or `processing`,
keep polling; once `completed`, the response carries the produced images, each with
a presigned `url` valid for **24 hours** — download the assets before they expire.
Only jobs your organization created are visible; any other id returns
[`404 not_found`](/guides/errors#not_found).
Prefer push over polling? Pass a `webhook_url` on the original
[generation request](/api-reference/images/generate) to receive a signed callback
the moment the job reaches a terminal status. See [Webhooks](/guides/webhooks).
# Image Generation
Source: https://docs.samsa.ai/api-reference/images/overview
Generate images from a prompt, optionally composing your organization's trained models.
Turn a text prompt into images with `POST /images/generations`, optionally
composing your organization's trained **style**, **object**, **person**, and
**setting** models plus a **color palette**. The default engine is
`nano_banana_pro` (pass `engine` to select `nano_banana_2`).
## Asynchronous pattern
Generation is asynchronous. `POST /images/generations` returns **`202 Accepted`**
immediately with a job `id`, `status`, and an `estimated_credits` figure. Poll
[`GET /images/generations/{id}`](/api-reference/images/get-generation) until
`status` is `completed` (or `failed`/`cancelled`), then read the produced images —
each carries a presigned URL valid for **24 hours**. Pass a `webhook_url` to be
notified instead of polling (see [Webhooks](/guides/webhooks)).
## Credits
Each output costs `5` credits at `1K`, scaling with resolution (`1K` ×1, `2K` ×2,
`4K` ×4) and multiplied by `num_outputs`. Costs draw from your organization's pool;
an exhausted pool returns `402`. See [Pricing](/guides/pricing).
A full zero-to-image walkthrough with curl, Python, and TypeScript.
# API reference
Source: https://docs.samsa.ai/api-reference/introduction
The Samsa public REST API — base URL, authentication, and conventions.
This page covers the conventions that apply to every endpoint. The full,
endpoint-by-endpoint reference — generated from the API's OpenAPI specification —
is in the groups in the sidebar:
[Image Generation](/api-reference/images/overview),
[Image Editing](/api-reference/edits/overview),
[Image Operations](/api-reference/image-ops/overview),
[Video Generation](/api-reference/videos/overview),
[Model Training](/api-reference/model-training/overview),
[Models](/api-reference/models/overview), and
[Account & Usage](/api-reference/account/overview). For a working end-to-end
example, see the [quickstart](/quickstart).
## Base URL
```
https://api.samsa.ai/public/v1
```
## Authentication
Every request must carry an organization API key as a bearer token:
```
Authorization: Bearer samsa_sk_your_key_here
```
Keys are organization-owned, scoped, and shown once at creation. See
[Create an API key](/quickstart#create-an-api-key) to get one.
## Conventions
* **Asynchronous jobs** — generation endpoints return `202 Accepted` with a job
`id`; poll the matching `GET` endpoint for status, or supply a `webhook_url` to
be notified on completion.
* **Statuses** — jobs move through `pending`, `processing`, and then a terminal
`completed`, `failed`, or `cancelled`.
* **Scopes** — most endpoints require a scope on the key (for example,
`images.generate`); `GET /me` needs only a valid key. A key missing a required
scope receives `403`.
* **Errors** — non-2xx responses return a structured JSON error body with a
`type`, `code`, `message`, and `request_id`.
* **Credits** — actions draw from the organization's credit pool; an exhausted
pool returns `402`.
* **Rate limits** — requests are limited per key; exceeding the limit returns
`429` with a `Retry-After` header.
# Complete a model upload
Source: https://docs.samsa.ai/api-reference/model-training/complete
api-reference/openapi.json POST /models/{model_id}/complete
Finalize a prepared model after uploading its images to the presigned URLs.
Step two of the presigned upload flow. After `PUT`-ing each file to the URLs from
[`POST /models/prepare`](/api-reference/model-training/prepare), call this endpoint
with the `uploaded_keys` (the R2 keys you uploaded, in order — first = cover). Samsa
validates the keys and starts processing, returning **`202 Accepted`**. Poll
[`GET /models/{id}/status`](/api-reference/model-training/status) until `completed`.
Requires the `models.write` scope.
Pass a `webhook_url` here to be notified when the model reaches a terminal status
instead of polling. See [Webhooks](/guides/webhooks).
# Create a model
Source: https://docs.samsa.ai/api-reference/model-training/create
api-reference/openapi.json POST /models
Create a model from 1–10 reference images supplied as URLs or inline base64.
Create a model from reference images supplied inline. The call returns
**`202 Accepted`** with a model `id`; poll
[`GET /models/{id}/status`](/api-reference/model-training/status) until `completed`.
Each of the 1–10 `images` is **either** an `https` `url` (downloaded server-side
under SSRF guards) **or** inline `base64` + `mime_type` (`image/jpeg`, `image/png`,
or `image/webp`, ≤ 10 MB each). The optional `instruction` is the model's
always-applied guidance — injected as mandatory ("MUST FOLLOW") direction into
every generation that composes the model, not a passive usage note (edit it later
with [`PATCH /models/{id}`](/api-reference/models/update)). Requires the
`models.write` scope.
## Example: create a style model from URLs
```bash curl theme={null}
curl -X POST https://api.samsa.ai/public/v1/models \
-H "Authorization: Bearer $SAMSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Brand Style",
"category": "style",
"images": [
{ "url": "https://cdn.example.com/ref-0.png" },
{ "url": "https://cdn.example.com/ref-1.png" },
{ "url": "https://cdn.example.com/ref-2.png" }
],
"instruction": "Always render on a clean, seamless white background.",
"webhook_url": "https://api.example.com/hooks/samsa"
}'
```
```json Body theme={null}
{
"name": "Acme Brand Style",
"category": "style",
"images": [
{ "url": "https://cdn.example.com/ref-0.png" },
{ "url": "https://cdn.example.com/ref-1.png" },
{ "url": "https://cdn.example.com/ref-2.png" }
],
"instruction": "Always render on a clean, seamless white background.",
"webhook_url": "https://api.example.com/hooks/samsa"
}
```
Each image is **either** a `url` **or** `base64` + `mime_type`, not both. For large
files, use the [presigned upload flow](/api-reference/model-training/prepare)
instead. `category` is one of `style`, `object`, `person`, `setting`.
# Model Training
Source: https://docs.samsa.ai/api-reference/model-training/overview
Create custom models — style, object, person, or setting — from reference images.
Create a custom model in one of four categories — `style`, `object`, `person`, or
`setting` — from 1–10 reference images. Once `completed`, the model can be composed
into [image generation](/api-reference/images/generate),
[Magic Edit](/api-reference/edits/create), and
[styled video](/api-reference/videos/generate).
## Two ways to submit reference images
* **Inline** — [`POST /models`](/api-reference/model-training/create) with each
image as an `https` `url` or inline `base64` + `mime_type`. Simplest for small
images and remote URLs.
* **Presigned upload** — for large files, call
[`POST /models/prepare`](/api-reference/model-training/prepare) to get presigned
PUT URLs, `PUT` each file, then
[`POST /models/{id}/complete`](/api-reference/model-training/complete) to start
processing.
Both paths require the `models.write` scope.
## Always-applied instruction
Each model carries an optional **`instruction`** — always-applied guidance (max
8000 characters). Unlike a per-request prompt, it is injected as mandatory
("MUST FOLLOW") direction into **every** image and video generation that composes
the model, so brand rules ("always a seamless white background") hold without
repeating them on each call. It is distinct from the model's `default_prompt` — an
AI-generated capability description that is also appended at generation. Set it at
creation and edit it any time with
[`PATCH /models/{id}`](/api-reference/models/update); on read it is returned as
`user_instruction`.
## Asynchronous pattern
Creation returns **`202 Accepted`** with a model `id`; poll
[`GET /models/{id}/status`](/api-reference/model-training/status) until `status` is
`completed` (or `failed`). A `webhook_url` gives you a push callback — see
[Webhooks](/guides/webhooks). Model creation is **free** — its `estimated_credits`
is `0` — but it still requires an active organization subscription; an organization
with no usable subscription returns `402`. See [Pricing](/guides/pricing).
# Prepare a model upload
Source: https://docs.samsa.ai/api-reference/model-training/prepare
api-reference/openapi.json POST /models/prepare
Request presigned PUT URLs for a new model's reference images (two-step upload).
Step one of the presigned upload flow for large files. Send the model `name`,
`category`, and metadata for the 1–10 `files` you will upload (the **first** file is
the cover). The response returns a model `id` and one presigned PUT `upload_url` per
file. `PUT` each file to its URL, then call
[`POST /models/{id}/complete`](/api-reference/model-training/complete) with the
uploaded keys to start processing. Requires the `models.write` scope.
Use this two-step flow when images are too large to send inline. For small images
or remote URLs, [`POST /models`](/api-reference/model-training/create) is simpler.
Each file's `content_type` is one of `image/jpeg`, `image/png`, `image/webp`;
provide `size` (≤ 10 MB) when you know it.
# Get model status
Source: https://docs.samsa.ai/api-reference/model-training/status
api-reference/openapi.json GET /models/{model_id}/status
Poll a model's creation lifecycle status.
Poll a model's creation status. Returns the lifecycle `status`
(`pending`, `processing`, `completed`, or `failed`) and, on failure, the `error`
detail. Unknown or other-organization ids return
[`404 not_found`](/guides/errors#not_found). Requires the `models.read` scope.
Once `status` is `completed`, the model is ready to compose into
[image generation](/api-reference/images/generate),
[Magic Edit](/api-reference/edits/create), and
[styled video](/api-reference/videos/generate).
# Delete a model
Source: https://docs.samsa.ai/api-reference/models/delete
api-reference/openapi.json DELETE /models/{model_id}
Soft-delete one of your organization's models.
Soft-deletes a model owned by your organization — it stops appearing in the app and
the API and returns **`204 No Content`**. Unknown, other-organization,
already-deleted, or internal models return
[`404 not_found`](/guides/errors#not_found). Requires the `models.write` scope.
Deletion is a soft-delete but is not reversible through the API. A model in use by
saved presets or automations will no longer be composable once deleted.
# Get a model
Source: https://docs.samsa.ai/api-reference/models/get
api-reference/openapi.json GET /models/{model_id}
Retrieve full detail for one of your organization's models.
Returns full detail for a single model owned by your organization, including
presigned reference-image URLs, the default prompt, trigger words, and the model's
`user_instruction`. That `user_instruction` is the model's **always-applied
guidance**: it is injected as mandatory ("MUST FOLLOW") direction into every image
and video generation that composes the model — not passive metadata. It contrasts
with `default_prompt`, an AI-generated capability description that is also appended
at generation. Set or change it via
[`PATCH /models/{model_id}`](/api-reference/models/update). Unknown,
other-organization, or deleted ids return
[`404 not_found`](/guides/errors#not_found). Requires the `models.read` scope.
# List models
Source: https://docs.samsa.ai/api-reference/models/list
api-reference/openapi.json GET /models
List your organization's models, newest first, with category/status filters and paging.
Returns a page of the models owned by your organization, most recently created
first. Filter by `category` (`style`, `object`, `person`, `setting`) and/or `status`,
and paginate with `limit` and `offset`. The response includes a `pagination` object
(`limit`, `offset`, `has_more`). Requires the `models.read` scope.
# Models
Source: https://docs.samsa.ai/api-reference/models/overview
List, retrieve, update, and delete your organization's models.
Manage your organization's model catalog. These endpoints read and edit models that
already exist — to create one, see
[Model Training](/api-reference/model-training/overview).
* [`GET /models`](/api-reference/models/list) — list your organization's models,
most recent first; filter by `category` and/or `status`, paginate with
`limit`/`offset`.
* [`GET /models/{id}`](/api-reference/models/get) — full detail for one model,
including presigned reference-image URLs, the default prompt, and trigger words.
* [`PATCH /models/{id}`](/api-reference/models/update) — update the editable fields
(`name`, `default_prompt`).
* [`DELETE /models/{id}`](/api-reference/models/delete) — soft-delete a model so it
stops appearing in the app and the API.
Reads require the `models.read` scope; `PATCH` and `DELETE` require `models.write`.
Ownership and privacy fields cannot be changed, and an id owned by another
organization returns [`404 not_found`](/guides/errors#not_found).
# Update a model
Source: https://docs.samsa.ai/api-reference/models/update
api-reference/openapi.json PATCH /models/{model_id}
Update the editable fields (name, default prompt, instruction) of one of your models.
Updates the editable fields of a model owned by your organization and returns the
updated model. `name`, `default_prompt`, and `instruction` are editable; ownership
and privacy fields cannot be changed. `instruction` is the model's **always-applied
guidance** (max 8000 characters), injected as mandatory ("MUST FOLLOW") direction
into every image and video generation that composes the model — it maps to the
`user_instruction` returned when you [read the model](/api-reference/models/get).
Omit a field to leave it unchanged — do not send it as an explicit `null` (send an
empty string to clear `instruction`). Unknown or other-organization ids return
[`404 not_found`](/guides/errors#not_found). Requires the `models.write` scope.
# Generate a video
Source: https://docs.samsa.ai/api-reference/videos/generate
api-reference/openapi.json POST /videos/generations
Submit a video job in image-to-video, text-to-video, or text-to-video-styled mode.
Submit a video-generation job. The `mode` field selects the request shape
(`image_to_video`, `text_to_video`, or `text_to_video_styled`) and the call returns
**`202 Accepted`** with a job `id`; poll
[`GET /videos/generations/{id}`](/api-reference/videos/get-generation) for the
result. Check [`GET /videos/models`](/api-reference/videos/list-engines) for each
engine's supported `duration`, `resolution`, `aspect_ratio`, end-frame, and audio
support.
## Example: image-to-video with a start and end frame
In `image_to_video` mode, `image` is the start frame and `end_image` an optional
end frame (only on end-frame-capable engines — see `supports_end_frame` in
[`GET /videos/models`](/api-reference/videos/list-engines)). Each frame takes
**exactly one** of `image_id`, `url`, or `base64` + `mime_type`.
```bash curl theme={null}
curl -X POST https://api.samsa.ai/public/v1/videos/generations \
-H "Authorization: Bearer $SAMSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"mode": "image_to_video",
"image": { "url": "https://cdn.example.com/start-frame.png" },
"end_image": { "url": "https://cdn.example.com/end-frame.png" },
"prompt": "The camera slowly pans right as waves roll in",
"engine": "veo_3_1_lite",
"duration": 8,
"aspect_ratio": "16:9",
"resolution": "720p"
}'
```
```json Body theme={null}
{
"mode": "image_to_video",
"image": { "url": "https://cdn.example.com/start-frame.png" },
"end_image": { "url": "https://cdn.example.com/end-frame.png" },
"prompt": "The camera slowly pans right as waves roll in",
"engine": "veo_3_1_lite",
"duration": 8,
"aspect_ratio": "16:9",
"resolution": "720p"
}
```
`duration` is required and must be one the engine supports (see `durations` in
[`GET /videos/models`](/api-reference/videos/list-engines)) — an unsupported value
returns `422`. For `text_to_video_styled`, `style_id` is required alongside
`prompt` and `duration`; the request body dropdown above shows all three modes.
# Get a video generation
Source: https://docs.samsa.ai/api-reference/videos/get-generation
api-reference/openapi.json GET /videos/generations/{generation_id}
Poll a video-generation job for status and, once completed, the produced video.
Retrieve the status of a video-generation job. While it is `pending` or
`processing`, keep polling; once `completed`, the response carries the produced
video with a presigned `url` valid for **24 hours**. Only jobs your organization
created are visible; any other id returns
[`404 not_found`](/guides/errors#not_found).
Pass a `webhook_url` on the original [video request](/api-reference/videos/generate)
to receive a signed callback instead of polling. See [Webhooks](/guides/webhooks).
# List video engines
Source: https://docs.samsa.ai/api-reference/videos/list-engines
api-reference/openapi.json GET /videos/models
The public video engine catalog — modes, durations, resolutions, and pricing per engine.
Returns the catalog of available video engines. Each entry lists the modes it
supports, valid `durations` (and `durations_by_resolution` /
`durations_with_end_frame` where they differ), `resolutions`, `aspect_ratios`,
whether it supports an end frame or audio, and its credit pricing
(`credits_per_second`, `resolution_multipliers`, `audio_credit_multiplier`).
Read this endpoint before calling
[`POST /videos/generations`](/api-reference/videos/generate) so you pass a valid
`engine` + `duration` + `resolution` combination — unsupported values return `422`.
# Video Generation
Source: https://docs.samsa.ai/api-reference/videos/overview
Produce video from a start frame, from text, or from text styled with your trained models.
`POST /videos/generations` produces a video in one of three modes, selected by the
`mode` field:
* **`image_to_video`** — animate a start frame, with an optional end frame on
end-frame-capable engines.
* **`text_to_video`** — generate a clip from a prompt alone.
* **`text_to_video_styled`** — a prompt plus your organization's trained models.
Engine capabilities (supported modes, durations, resolutions, aspect ratios,
end-frame and audio support, and pricing) come from
[`GET /videos/models`](/api-reference/videos/list-engines) — read it first to pick a
valid `engine` + `duration` combination.
## Asynchronous pattern
`POST /videos/generations` returns **`202 Accepted`** with a job `id`; poll
[`GET /videos/generations/{id}`](/api-reference/videos/get-generation) until
`completed`, then read the produced video (presigned URL valid **24 hours**). A
`webhook_url` gives you a push callback — see [Webhooks](/guides/webhooks).
## Credits
Video credits scale with the engine's per-second rate, duration, resolution
multiplier, and audio multiplier — see `credits_per_second` and the multipliers in
[`GET /videos/models`](/api-reference/videos/list-engines). Costs draw from your
organization's pool; an exhausted pool returns `402`. See
[Pricing](/guides/pricing).
# Changelog
Source: https://docs.samsa.ai/changelog
New endpoints, new MCP tools, and behaviour changes in the Samsa public API — newest first.
Everything that changed in the [REST API](/api), the [MCP server](/mcp-server), and
the [content verification API](/content-verification/overview). Dates are when the
change reached production.
### Image operations
Six ways to transform an image you already have, each an async `POST` that
submits a job plus a `GET` that polls it.
* **`POST /images/img2img`** · **`GET /images/img2img/{img2img_id}`** — transform
1–14 source images with a prompt.
* **`POST /images/variations`** · **`GET /images/variations/{variation_id}`** —
creative variations of one image, targeted at the whole frame, a person, an
object, or the scene.
* **`POST /images/resizes`** · **`GET /images/resizes/{resize_id}`** — change
aspect ratio by outpainting rather than cropping.
* **`POST /images/upscales`** · **`GET /images/upscales/{upscale_id}`** — upscale
to a higher resolution across four models.
* **`POST /images/background-removals`** ·
**`GET /images/background-removals/{background_removal_id}`** — cut the
background to a transparent PNG.
* **`POST /images/vectorizations`** ·
**`GET /images/vectorizations/{vectorization_id}`** — trace an image to SVG.
**New scope.** Variations, resizes, upscales, background removals, and
vectorizations require `images.transform`. Img2img runs under the existing
`images.edit`. Existing keys created with all scopes already carry it; a
narrowed key needs the scope added in **Settings → API Keys**.
**Also in MCP.** The same six operations are available as the `img2img`,
`create_variations`, `resize_image`, `upscale_image`, `remove_background`, and
`vectorize_image` tools.
**Pricing.** Background removal costs 1 credit and vectorization 5, both `0` on a
cache hit for a source already processed. The rest bill at `5 × resolution ×
outputs`; upscale bills by target tier times a per-model multiplier.
[Image Operations](/api-reference/image-ops/overview) ·
[Pricing](/guides/pricing)
### Hosted image watermark decode
The content verification API now decodes both image watermark techniques itself,
rather than reporting them as unchecked.
* **TrustMark Q** — the legacy corpus, covering images marked while TrustMark was
the embed vendor. This lane stays available for that corpus permanently.
* **Meta PixelSeal** — the current image watermark carried by newly generated
Samsa images.
Watermarks remain corroboration only: the `detected` verdict still comes from
C2PA manifest verification, which is authoritative. Video watermark decode
(Meta Video Seal) is **not yet available** and is reported as `not_checked` — a
technique that was never evaluated, never a false negative.
[Content verification](/content-verification/overview)
### Content verification API
A free, unauthenticated API for checking whether an image or video was made with
Samsa — Samsa's public detection mechanism under **Article 50(2) of the EU AI
Act**. It has its own base URL, takes no API key, and spends no credits.
```
https://detect.samsa.ai
```
* **`POST /v1/public/detect`** — check one uploaded image or video.
* **`GET /v1/public/detect/info`** — machine-readable description of each marking
technique and how to detect it.
* **`GET /v1/public/detect/result/{request_id}`** — signed PDF of a past result.
* **`GET /v1/public/detect/health`** — service status and version.
[Content verification](/content-verification/overview)
### Trained models by name, palettes, and inline previews
Three changes that make the MCP tools easier to drive from a conversation, where
a model's name is at hand but its id is not.
* **Names resolve like ids.** `style_id`, `object_ids`, `person_ids`, and
`setting_ids` accept a trained model's **name or id**; a name resolves to a
model visible to the connected credential.
* **`color_palette`** — a new parameter on `generate_image` and `edit_image`,
also given as a name or an id.
* **`edit_image` reached parity** with `generate_image`, accepting all four
trained-model reference parameters so an edit stays on-brand.
* **`get_job_status` returns a preview.** A completed raster image job now
carries a downscaled inline image alongside the link to the full-resolution
asset, so clients can render the result directly. Vectorization returns SVG and
has no raster preview.
[MCP server](/mcp-server)
### The Samsa public API and MCP server
The first public release. Samsa's image and video studio — and your
organization's trained models — over a REST API and a remote MCP server.
```
https://api.samsa.ai/public/v1
```
* **Images** — `POST /images/generations` and `POST /images/edits`, composing
your organization's trained **style**, **object**, **person**, and **setting**
models.
* **Video** — `POST /videos/generations` from a start frame, from text, or from
text styled with your models, plus `GET /videos/models` for the engine list.
* **Model training** — `POST /models`, `POST /models/prepare`,
`POST /models/{model_id}/complete`, and `GET /models/{model_id}/status`.
* **Models** — list, retrieve, update, and delete at `/models`.
* **Account** — `GET /me`, `GET /credits`, and `GET /usage`.
**Organization API keys.** Scoped, shown once at creation, and acting for the
key's organization — credits come from that organization's pool.
**Webhooks.** Pass a `webhook_url` to be notified on completion instead of
polling. Callbacks are signed, and failures retry at 5s, 30s, 2m, 15m, and 1h.
**MCP server.** A remote Streamable-HTTP server at `https://api.samsa.ai/mcp`,
taking either OAuth 2.1 for interactive clients such as Claude and ChatGPT or an
API key for headless ones.
[Samsa API](/api) · [Quickstart](/quickstart) ·
[MCP server](/mcp-server)
# Check an image or video
Source: https://docs.samsa.ai/content-verification/detect
Upload one asset and get a per-technique detection result, with the C2PA verdict as the authoritative answer.
```
POST https://detect.samsa.ai/v1/public/detect
```
Upload a single image or video as `multipart/form-data`. The response carries one
top-level `detected` verdict plus a per-technique breakdown. No authentication, no
credits. The upload is processed **in memory and never retained**.
## Request
Send exactly **one** file part named `file`. The part must carry a `filename` in its
`Content-Disposition` header — a plain form field without one is not a file part.
| | |
| ----------------- | ------------------------------------ |
| **Content-Type** | `multipart/form-data` |
| **Field** | `file` — the image or video to check |
| **Max size** | 50 MB |
| **Image formats** | JPEG, PNG, WebP, GIF |
| **Video formats** | MP4, QuickTime, WebM |
JPEG/PNG/WebP/GIF and MP4/QuickTime are recognised by magic bytes, so a wrong or
generic part `Content-Type` (for example `application/octet-stream`) does not break
detection for those formats. **WebM has no magic-byte branch** and is accepted only
when the part declares `Content-Type: video/webm`.
```bash curl theme={null}
curl -X POST https://detect.samsa.ai/v1/public/detect \
-F "file=@photo.jpg"
```
```python Python theme={null}
import requests
with open("photo.jpg", "rb") as fh:
response = requests.post(
"https://detect.samsa.ai/v1/public/detect",
files={"file": ("photo.jpg", fh, "image/jpeg")},
timeout=120,
)
response.raise_for_status()
result = response.json()
print(result["detected"])
```
```typescript TypeScript theme={null}
import { readFile } from "node:fs/promises";
const form = new FormData();
// The third argument sets the part filename — required for it to count as a file part.
form.append("file", new Blob([await readFile("photo.jpg")]), "photo.jpg");
const response = await fetch("https://detect.samsa.ai/v1/public/detect", {
method: "POST",
body: form,
});
if (!response.ok) {
const { detail } = await response.json();
throw new Error(`Detection failed (${response.status}): ${detail}`);
}
const result = await response.json();
console.log(result.detected);
```
## Response
`200 OK`. An image or video that carries a valid, trusted Samsa C2PA manifest:
```json Response theme={null}
{
"request_id": "c1a2b3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"detected": "samsa",
"techniques": [
{
"type": "metadata",
"technique_id": "c2pa",
"result": "samsa",
"confidence": "high",
"manifest": {
"claim_generator": "Samsa/1.0",
"digital_source_type": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia",
"upstream_provider": "fal",
"upstream_model": "flux-pro",
"timestamp": "2026-07-02T14:21:07+00:00",
"assertions": ["c2pa.actions", "c2pa.hash.data"]
}
},
{
"type": "watermark",
"technique_id": "trustmark-q",
"result": "absent",
"confidence": "low",
"vendor_id": null
},
{
"type": "watermark",
"technique_id": "pixelseal",
"result": "samsa",
"confidence": "high",
"vendor_id": "samsa-pixelseal-v1"
}
],
"external_verification": {
"c2pa": "https://verify.contentauthenticity.org"
},
"result_pdf_url": "https://detect.samsa.ai/v1/public/detect/result/c1a2b3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d/pdf",
"responded_at": "2026-07-02T14:21:09.412093Z"
}
```
| Field | Type | Description |
| ----------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------- |
| `request_id` | string (UUID) | Server-generated id for this check. Use it to fetch the [signed result PDF](/content-verification/results). |
| `detected` | enum | The top-level verdict: `samsa`, `non_samsa`, or `unknown`. |
| `techniques[]` | array | One entry per technique that was applied — see [below](#reading-the-techniques-breakdown). |
| `external_verification` | object | Links for verifying the result independently of Samsa. Currently `c2pa`. |
| `result_pdf_url` | string \| null | Link to the signed PDF of this result, or `null` when result persistence is unavailable. |
| `responded_at` | string (ISO 8601) | When the check completed, in UTC. |
`result_pdf_url` may be `null`. When it is present, follow it exactly as returned
rather than assembling the URL yourself — the returned URL and the
[documented result endpoint](/content-verification/results) serve the identical
signed PDF.
### The `detected` verdict
The verdict comes from C2PA manifest verification, which is authoritative. Watermark
techniques are corroboration: they can supply attribution when no manifest is present,
but they never override a manifest verdict.
| `detected` | Reached when |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `samsa` | A valid, trusted, allowlisted Samsa manifest was verified — or, with no manifest present, a watermark decode returned a Samsa hit. |
| `non_samsa` | A manifest is present but is not attributable to Samsa (another issuer, untrusted, or tampered). |
| `unknown` | No manifest is present and no watermark hit was returned. |
`unknown` means **this check found no evidence**, not that the asset is
authentic or human-made. Provenance metadata is easily stripped, and for video the
hosted watermark decode is [not yet available](#reading-the-techniques-breakdown).
## Reading the techniques breakdown
`techniques[]` always starts with the single `metadata` entry (the authoritative C2PA
layer), followed by the watermark techniques applicable to the asset's modality.
### The `metadata` technique
```json theme={null}
{
"type": "metadata",
"technique_id": "c2pa",
"result": "absent",
"confidence": "low",
"manifest": null
}
```
| Field | Description |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `technique_id` | Always `c2pa` for this entry. |
| `result` | `samsa` (valid, trusted, allowlisted Samsa manifest), `non_samsa` (a manifest from another issuer, or one that is valid but untrusted), `tampered` (a manifest that fails validation), or `absent` (no manifest — commonly because it was stripped). |
| `confidence` | `high`, `medium`, or `low`. Always present for this technique. |
| `manifest` | A layperson-readable summary of the verified manifest (`claim_generator`, `digital_source_type`, `upstream_provider`, `upstream_model`, `timestamp`, `assertions`), or `null` when there is nothing to summarize. |
C2PA manifest verification is **live**: it is served by the Samsa public API and by any
independent C2PA validator.
### Watermark techniques
```json theme={null}
{
"type": "watermark",
"technique_id": "pixelseal",
"result": "samsa",
"confidence": "high",
"vendor_id": "samsa-pixelseal-v1"
}
```
| Field | Description |
| -------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `technique_id` | `trustmark-q` or `pixelseal` for an image, `video-seal` for a video. Always present, including for `not_checked`. |
| `result` | `samsa`, `absent`, `low_confidence`, or `not_checked` — see the table below. |
| `confidence` | `high`, `medium`, or `low`, and `null` **if and only if** `result` is `not_checked`. The key is always present. |
| `vendor_id` | Identifies the decoder that produced a hit (`samsa-trustmark-v1`, `samsa-pixelseal-v1`); `null` unless `result` is `samsa`. |
| `result` | Meaning |
| ---------------- | --------------------------------------------------------------------------------- |
| `samsa` | A decoder ran and found a Samsa watermark. |
| `absent` | A decoder ran and found no Samsa watermark. |
| `low_confidence` | A decoder ran and returned a weak hit. |
| `not_checked` | The decoder backend is not provisioned, so **the technique was never evaluated**. |
Hosted decode is **live for both image techniques**, and their confidence semantics
differ by design:
* **TrustMark Q** (`trustmark-q`) covers the legacy corpus — images marked while
TrustMark was the embed vendor — and stays available for that corpus permanently.
Confidence describes the match: `high` is an exact payload match, `medium` a match
within the code's error-correction radius, and a weak residual signal reports
`low_confidence` — never a false “no watermark”.
* **Meta PixelSeal** (`pixelseal`) is the current image watermark — newly generated
Samsa images carry PixelSeal. Presence is decided by bit accuracy against the vendor
payload at a fixed threshold; below the threshold the result is `absent`. This lane
deliberately has **no `low_confidence` band**.
`not_checked` is **not** a statement that no watermark is present — that is what
`absent` means. Hosted **video** watermark decode is not yet available; it is
scheduled before 2 February 2027. Until then the API reports the `video-seal`
technique as `not_checked`, never as a false “no watermark”. Never render a
`not_checked` technique as an absent watermark.
In every lane, a decoder that cannot run reports `not_checked`, never a constructed
`absent`. For the algorithm IDs, pinned model artifact hashes, per-lane soft-binding
labels, and open-source decoders behind each technique, read
[How detection works](https://detect.samsa.ai/how-detection-works) or
[`GET /v1/public/detect/info`](/content-verification/info).
## Errors
Errors return `{"detail": "…"}` — not the Samsa REST API
[error envelope](/guides/errors).
| Status | When |
| ------ | ---------------------------------------------------------------------------------------------------------------- |
| `400` | Malformed multipart body, a missing boundary, an empty upload, or a file-part count other than exactly one. |
| `413` | The upload exceeds the 50 MB limit. |
| `415` | The request is not `multipart/form-data`, or the uploaded bytes are not a supported image or video. |
| `503` | An available decoder or the C2PA verifier failed at inference time. The response carries a `Retry-After` header. |
Treat `503` as transient and retry after the `Retry-After` interval. A decoder that is
simply not provisioned never produces a `503` — it degrades to `not_checked` in a
normal `200` response.
# Get detection info
Source: https://docs.samsa.ai/content-verification/info
A machine-readable description of every Samsa marking technique, its decoder, and its hosted-decode status.
```
GET https://detect.samsa.ai/v1/public/detect/info
```
Returns a static, machine-readable description of the detection service: the methods it
applies, its retention posture, external verification links, and — per marking
technique — everything a third party needs to integrate and access detection
independently of Samsa.
This is the JSON counterpart of
[How detection works](https://detect.samsa.ai/how-detection-works), Samsa's public
Measure 3.4(b) information for the European Commission and other stakeholders. The
endpoint takes no parameters, reads no client data, and returns no PII.
```bash curl theme={null}
curl https://detect.samsa.ai/v1/public/detect/info
```
## Response
| Field | Type | Description |
| ------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `service` | string | Service identifier. |
| `methods[]` | array | The detection methods applied, each with a `type` (`metadata` or `watermark`), a `role` (`authoritative` or `corroboration`), and a `description`. |
| `retention` | string | The retention posture, in prose. |
| `external_verification` | object | Links for verifying results independently of Samsa. Currently `c2pa`. |
| `result_pdf_availability_hours` | integer | How long a [signed result PDF](/content-verification/results) stays downloadable. |
| `marking_techniques[]` | array | Per-technique integration and access information — see [below](#marking-techniques). |
| `info_page_url` | string | URL of the human-readable version of this information. |
### Marking techniques
Each entry in `marking_techniques[]` describes one technique:
| Field | Type | Description |
| ------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `id` | string | Stable technique id: `c2pa`, `trustmark-q`, `pixelseal`, or `video-seal`. |
| `kind` | enum | `metadata` or `watermark`. |
| `modalities[]` | array | Which of `image` and `video` the technique applies to. |
| `display_name` | string | Human-readable name. |
| `description` | string | What the technique is and what role it plays in the verdict. |
| `algorithm_id` | string \| null | The C2PA-listed algorithm id. `null` for C2PA itself, which is a provenance manifest, not a soft-binding algorithm. |
| `soft_binding_label_mappings[]` | array | Per signing lane, the assertion `label` the soft-binding payload is carried under, with a `lane` name and explanatory `detail`. |
| `decoder` | string \| null | Where to get an independent decoder for this technique. |
| `pinned_artifacts` | object | SHA-256 of the model artifacts Samsa has pinned, keyed by role (for example `encoder`, `decoder`). |
| `hosted_decode_status` | enum | `available` or `not_yet_available` — the status of **Samsa's hosted detector** for this technique. |
| `hosted_decode_detail` | string | The status in prose. |
| `notes` | string \| null | A per-technique caveat, where one applies. |
| `info_url` | string | URL of the human-readable description of this technique. |
`hosted_decode_status` describes **the detector**, not the marking. Both image
watermark techniques report `available` — hosted image watermark decode is live. The
video technique reports `not_yet_available`: hosted video watermark decode is
scheduled before 2 February 2027, and until then the detect endpoint reports it as
`not_checked`, never as a false “no watermark”.
```json Response theme={null}
{
"service": "daku-detect",
"methods": [
{
"type": "metadata",
"role": "authoritative",
"description": "C2PA manifest verification is the authoritative provenance signal: a valid, trusted, allowlisted Samsa manifest identifies content as created with Samsa."
},
{
"type": "watermark",
"role": "corroboration",
"description": "Invisible watermark decodes provide best-effort corroboration and degrade gracefully when a decoder backend is unavailable; they never override the C2PA verdict."
}
],
"retention": "Samsa does not retain uploaded content: uploads are processed in memory and are never persisted or logged. Only a no-PII audit row (request id, coarse region bucket, verdict facts) is recorded.",
"external_verification": {
"c2pa": "https://verify.contentauthenticity.org"
},
"result_pdf_availability_hours": 24,
"marking_techniques": [
{
"id": "c2pa",
"kind": "metadata",
"modalities": ["image", "video"],
"display_name": "C2PA Content Credentials",
"description": "Cryptographic provenance manifest embedded in the asset; the authoritative detection layer for both images and video.",
"algorithm_id": null,
"soft_binding_label_mappings": [],
"decoder": "Validate with any independent C2PA validator (c2patool, verify.contentauthenticity.org) or with our hosted API POST /v1/public/detect.",
"pinned_artifacts": {},
"hosted_decode_status": "available",
"hosted_decode_detail": "Live: C2PA manifest verification is served by our public API and by any independent C2PA validator.",
"notes": "Trust anchoring per lane, split by asset SIZE rather than media type: the Trufo lane covers images and video up to the inline size ceiling, and Trufo's C2PA Root CA is on the official C2PA Trust List, so those manifests chain to a trust-listed root; the own-cert lane covers re-signed assets and anything above that ceiling, carrying the same assertions and validating structurally against the Samsa signer chain. Samsa's own C2PA conformance application (generator product 'Samsa Media Service') is filed and under review.",
"info_url": "https://detect.samsa.ai/how-detection-works"
},
{
"id": "trustmark-q",
"kind": "watermark",
"modalities": ["image"],
"display_name": "TrustMark Q (image imperceptible watermark)",
"description": "Adobe TrustMark Q imperceptible image watermark; a best-effort corroboration signal that never overrides the authoritative C2PA verdict.",
"algorithm_id": "com.adobe.trustmark.Q",
"soft_binding_label_mappings": [
{
"lane": "Trufo-signed (assets up to the inline size ceiling)",
"label": "ai.samsa.soft-binding",
"detail": "The Trufo wire contract rejects c2pa.* custom labels, so the standard soft-binding payload (naming com.adobe.trustmark.Q) is carried under this Samsa RDNN label. Third parties MUST read this label to find the signpost on Trufo-signed assets."
},
{
"lane": "own-cert (re-signed assets and anything above the ceiling)",
"label": "c2pa.soft-binding",
"detail": "Own-cert-signed content carries the identical payload under the standard C2PA soft-binding label."
}
],
"decoder": "Adobe's open-source `trustmark` package (https://github.com/adobe/trustmark).",
"pinned_artifacts": {
"encoder": "80a3c3c0cb8cd16d0de4411ee7df122029424a0d5c510d875506adb90276bb9c",
"decoder": "28b177ef112965e7e4bf2153b06f4464ce5ced52b68e6da3bbf5e1d15084f110"
},
"hosted_decode_status": "available",
"hosted_decode_detail": "Live: hosted decode is served by our public API. Confidence describes the match, not the content: 'high' is an exact payload match, 'medium' a match within the code's error-correction radius, and a weak residual signal reports low_confidence. A decoder that cannot run reports not_checked (never a constructed 'absent' false negative).",
"notes": null,
"info_url": "https://detect.samsa.ai/how-detection-works"
},
{
"id": "pixelseal",
"kind": "watermark",
"modalities": ["image"],
"display_name": "Meta PixelSeal (image imperceptible watermark)",
"description": "Meta PixelSeal imperceptible image watermark; a best-effort corroboration signal that never overrides the authoritative C2PA verdict. Content marked with the TrustMark Q technique remains covered by that technique's row permanently.",
"algorithm_id": "com.aiwatermark.pixelseal.1",
"soft_binding_label_mappings": [
{
"lane": "Trufo-signed (assets up to the inline size ceiling)",
"label": "ai.samsa.soft-binding",
"detail": "The Trufo wire contract rejects c2pa.* custom labels, so the standard soft-binding payload (naming com.aiwatermark.pixelseal.1) is carried under this Samsa RDNN label. Third parties MUST read this label to find the signpost on Trufo-signed assets."
},
{
"lane": "own-cert (re-signed assets and anything above the ceiling)",
"label": "c2pa.soft-binding",
"detail": "Own-cert-signed content carries the identical payload under the standard C2PA soft-binding label."
}
],
"decoder": "Open-source facebookresearch/videoseal (https://github.com/facebookresearch/videoseal), model card `pixelseal`. The pinned serving artefact is a TorchScript export of that model; the detection payload is the full SHA-256 of the vendor id, matched by bit accuracy.",
"pinned_artifacts": {
"decoder": "eac1142d59e820448da7e6ba6336e94f41532cdcaccc7b3caf902848eee116ef",
"source-checkpoint": "0c5665cff20eb6ce1b5aaa7d91c19dafb418bfee32d02dd3344e4ed60d9d75bd"
},
"hosted_decode_status": "available",
"hosted_decode_detail": "Live: hosted decode is served by our public API. Presence is decided by bit accuracy against the vendor payload at a fixed threshold; below the threshold the API reports absent (this lane has no low_confidence band). A decoder that cannot run reports not_checked, never a constructed 'absent' false negative.",
"notes": "The PixelSeal watermark embed ships with the image vendor switch, a separate axis from the hosted detector: decode availability does not imply every image is PixelSeal-marked.",
"info_url": "https://detect.samsa.ai/how-detection-works"
},
{
"id": "video-seal",
"kind": "watermark",
"modalities": ["video"],
"display_name": "Meta Video Seal (video imperceptible watermark)",
"description": "Meta Video Seal imperceptible video watermark; a best-effort corroboration signal that never overrides the authoritative C2PA verdict.",
"algorithm_id": "com.aiwatermark.videoseal.1",
"soft_binding_label_mappings": [
{
"lane": "Trufo-signed (assets up to the inline size ceiling)",
"label": "ai.samsa.soft-binding",
"detail": "Signing-route selection is by asset SIZE, not by media type, so video signed through the Trufo lane carries the soft-binding under this Samsa RDNN label for the same reason images do (the Trufo wire contract rejects c2pa.* custom labels). Third parties MUST read this label as well as the standard one."
},
{
"lane": "own-cert (re-signed assets and anything above the ceiling)",
"label": "c2pa.soft-binding",
"detail": "Own-cert-signed video carries the identical payload under the standard C2PA soft-binding label."
}
],
"decoder": "Open-source facebookresearch/videoseal (https://github.com/facebookresearch/videoseal).",
"pinned_artifacts": {},
"hosted_decode_status": "not_yet_available",
"hosted_decode_detail": "Hosted watermark decode is not yet available in our public API; it lands with the watermark-interoperability work, before 2 February 2027.",
"notes": "The VideoSeal watermark embed itself goes live with the video lane once its validation completes, a separate axis from provisioning the public detector; do not read this as hosted decode being live.",
"info_url": "https://detect.samsa.ai/how-detection-works"
}
],
"info_page_url": "https://detect.samsa.ai/how-detection-works"
}
```
The same information as a human-readable page, including the trust-anchoring detail
behind each signing lane.
# Content verification
Source: https://docs.samsa.ai/content-verification/overview
The free, unauthenticated public API for checking whether an image or video was made with Samsa.
This API is **separate from the Samsa REST API**. It has its own base URL, takes no
API key, and spends no credits — so none of the
[API reference conventions](/api-reference/introduction) (bearer auth, scopes, the
error envelope, credit accounting) apply to it.
Samsa marks its AI-generated content with machine-readable techniques so it can be
detected later. This API is how you read those marks back: upload an image or a video,
and it reports what each technique found.
It is Samsa's public detection mechanism under **Article 50(2) of the EU AI Act**, and
it is free for anyone to use — platforms, newsrooms, researchers, regulators, or
anyone who has an asset and wants to check it.
## Base URL
```
https://detect.samsa.ai
```
No `Authorization` header, no API key, no credits.
## Endpoints
| Endpoint | Purpose |
| ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| [`POST /v1/public/detect`](/content-verification/detect) | Check one uploaded image or video. |
| [`GET /v1/public/detect/info`](/content-verification/info) | Machine-readable description of each marking technique and how to detect it. |
| [`GET /v1/public/detect/result/{request_id}`](/content-verification/results) | Signed PDF of a past result. |
| `GET /v1/public/detect/health` | Service status and version — see [Service health](#service-health). |
## How the verdict is decided
The response reports each technique separately, and one top-level `detected` verdict.
**The verdict comes from C2PA manifest verification**, which is authoritative.
Watermark techniques are corroboration only: they can add attribution when no manifest
is present, but they never override the C2PA verdict.
| Technique | Role | Applies to | Hosted decode in the Samsa API |
| ------------------------ | ------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| C2PA Content Credentials | Authoritative provenance layer | Images and video | **Live.** Served by the Samsa public API and by any independent C2PA validator. |
| TrustMark Q | Image watermark · corroboration | Images | **Live.** Covers the legacy corpus — images marked while TrustMark was the embed vendor. This lane stays available for that corpus permanently. |
| Meta PixelSeal | Image watermark · corroboration | Images | **Live.** The current image watermark — newly generated Samsa images carry PixelSeal. |
| Meta Video Seal | Video watermark · corroboration | Video | **Not yet available.** Hosted watermark decode is scheduled before 2 February 2027. |
Hosted watermark decode is live for both image techniques. For video it is not yet
available, so the API reports the video watermark technique as `not_checked`, never
as a false “no watermark”. A `not_checked` technique was **never evaluated** — it is
not a finding that no watermark is present.
See [Reading the techniques breakdown](/content-verification/detect#reading-the-techniques-breakdown).
The canonical, per-technique description: algorithm IDs, pinned model artifact
hashes, the per-lane soft-binding label mapping, decoders, and trust anchoring. It is
Samsa's public Measure 3.4(b) information for the European Commission and other
stakeholders. The same content is available as JSON from
[`GET /v1/public/detect/info`](/content-verification/info).
## Retention
Uploads are processed **in memory and never retained**. They are never persisted and
never logged. Only a no-PII audit row — a request reference, a coarse region bucket,
and the verdict facts — is recorded. Where a signed result PDF is issued, it stays
downloadable for **24 hours**.
## Service health
`GET /v1/public/detect/health` returns a minimal status and version payload. It reads
no client data and echoes no PII — use it for uptime probes.
```bash curl theme={null}
curl https://detect.samsa.ai/v1/public/detect/health
```
```json Response theme={null}
{
"status": "healthy",
"service": "daku-detect",
"version": "0.1.0",
"timestamp": "2026-07-25T06:32:42.089977+00:00"
}
```
## Errors
This service does **not** use the Samsa REST API's [error envelope](/guides/errors).
Failures return the plain shape below, and you should branch on the HTTP status:
```json theme={null}
{
"detail": "Unsupported media type: expected an image or video."
}
```
A `503` additionally carries a `Retry-After` header. Per-endpoint status codes are
listed on each endpoint page.
## Verify independently
You never have to take Samsa's word for a C2PA result. The manifest travels with the
asset, so any independent validator reads the same evidence.
Drop the asset into the public C2PA validator and inspect its manifest yourself.
The same technique-by-technique information as JSON, including decoder pointers.
# Download a result PDF
Source: https://docs.samsa.ai/content-verification/results
Fetch a digitally signed PDF record of a past detection result, available for 24 hours.
```
GET https://detect.samsa.ai/v1/public/detect/result/{request_id}
```
Returns a digitally signed PDF record of a past detection result. Use it when you need
a shareable, verifiable artifact of a check — a compliance record, an attachment to a
report, or evidence in a review.
The PDF is available for **24 hours** after the original check. It contains no PII: no
IP address, no user agent, no filename, and none of the uploaded bytes — only the
verdict facts, the `request_id`, a coarse region bucket, and the timestamp.
## Request
`request_id` is the UUID returned by
[`POST /v1/public/detect`](/content-verification/detect). The endpoint takes no
parameters and no authentication.
```bash curl theme={null}
curl -o result.pdf \
https://detect.samsa.ai/v1/public/detect/result/c1a2b3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d
```
The detect response also carries a ready-made `result_pdf_url`. **Follow that URL
exactly as returned** rather than assembling one yourself — it and the path above
serve the identical signed PDF. `result_pdf_url` is `null` when result persistence is
unavailable; in that case there is no PDF to fetch.
## Response
`200 OK` with `Content-Type: application/pdf` and the following headers:
| Header | Value |
| ------------------------ | --------------------------------------------------------- |
| `Content-Disposition` | `attachment; filename="samsa-detection-.pdf"` |
| `Cache-Control` | `private, max-age=` |
| `X-Content-Type-Options` | `nosniff` |
The document restates the verdict, the per-technique outcome, the record metadata
(`request_id`, response timestamp, coarse region bucket), and the independent
verification link.
## Signature
The PDF carries a PAdES-style detached PKCS#7/CMS signature, applied as a PDF
incremental update — so the file re-parses as an ordinary PDF and any standard PDF
reader can open it. It is signed with the same signer identity Samsa's C2PA signing
path uses, so authenticity is verifiable against the same certificate chain. A
tampered byte or a wrong CA root fails verification.
If the signer is not configured, the endpoint returns `503` rather than an unsigned
PDF — an unsigned result is never served.
## Errors
Errors return `{"detail": "…"}` — not the Samsa REST API
[error envelope](/guides/errors).
| Status | When |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `404` | The `request_id` is unknown or is not a UUID. |
| `410` | The result is older than the 24-hour availability window. The record may persist for retention purposes, but the PDF is issued for 24 hours only. |
| `503` | The detection store or the result signer is temporarily unavailable. The response carries a `Retry-After` header — retry. |
Fetch the PDF while it is fresh and archive it on your side if you need it beyond
24 hours. Once the window closes the endpoint returns `410` and the document cannot
be reissued.
# Authentication
Source: https://docs.samsa.ai/guides/authentication
Organization API keys, scopes, and the Bearer header every Samsa API request needs.
Every request to the Samsa API is authenticated with an **organization API key**.
Keys are scoped, shown once, and act for the organization that owns them — credits
are drawn from that organization's pool and generated assets appear in the app under
the account of the admin who created the key.
Connecting Samsa to an MCP client (Claude, ChatGPT, Claude Code, Cursor…)? The
[MCP server](/mcp-server) uses these same API keys for headless clients and OAuth 2.1
sign-in for interactive ones — the scopes below apply to MCP tools identically.
## How keys work
* **Organization-owned.** A key belongs to an organization, not a person. Anyone
holding the key acts for that organization.
* **Admin-created.** Only an organization **admin** (`OWNER` or `ADMIN`) can create
or revoke keys, in the app's **Settings → API Keys** tab.
* **Scoped.** Each key carries a set of [scopes](#scopes) that determine which
endpoints it can call. Keys are created with all scopes by default; narrow them to
match what the integration needs.
* **Shown once.** The full secret is displayed exactly once, at creation.
Samsa stores only a **SHA-256 hash** of each key, never the plaintext. That is why
a key can never be shown again or recovered — there is nothing to recover from. If
you lose a key, [revoke it and create a new one](#rotating-a-key).
## The `Authorization` header
Send your key as a **Bearer token** on every request:
```
Authorization: Bearer samsa_sk_your_key_here
```
```bash curl theme={null}
curl https://api.samsa.ai/public/v1/me \
-H "Authorization: Bearer $SAMSA_API_KEY"
```
```python Python theme={null}
import os
import requests
BASE_URL = "https://api.samsa.ai/public/v1"
headers = {"Authorization": f"Bearer {os.environ['SAMSA_API_KEY']}"}
resp = requests.get(f"{BASE_URL}/me", headers=headers)
resp.raise_for_status()
print(resp.json())
```
```typescript TypeScript theme={null}
const BASE_URL = "https://api.samsa.ai/public/v1";
const headers = { Authorization: `Bearer ${process.env.SAMSA_API_KEY}` };
const resp = await fetch(`${BASE_URL}/me`, { headers });
if (!resp.ok) throw new Error(`GET /me failed: ${resp.status}`);
console.log(await resp.json());
```
Use [`GET /me`](/quickstart) to confirm a key works — it
returns the key's organization, its safe metadata (prefix, scopes, expiry), and the
organization's available credit balance, but never the secret.
## Scopes
Scopes follow a `.` shape. A request to an endpoint whose scope the
key lacks fails with [`403 missing_scope`](/guides/errors#missing_scope).
| Scope | Endpoints unlocked |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `images.generate` | `POST /images/generations`, `GET /images/generations/{id}` |
| `images.edit` | `POST /images/edits`, `GET /images/edits/{id}`, `POST /images/img2img`, `GET /images/img2img/{id}` |
| `images.transform` | `POST /images/variations`, `GET /images/variations/{id}`, `POST /images/upscales`, `GET /images/upscales/{id}`, `POST /images/resizes`, `GET /images/resizes/{id}`, `POST /images/background-removals`, `GET /images/background-removals/{id}`, `POST /images/vectorizations`, `GET /images/vectorizations/{id}` |
| `videos.generate` | `POST /videos/generations`, `GET /videos/generations/{id}`, `GET /videos/models` |
| `models.read` | `GET /models`, `GET /models/{id}`, `GET /models/{id}/status` |
| `models.write` | `POST /models`, `POST /models/prepare`, `POST /models/{id}/complete`, `PATCH /models/{id}`, `DELETE /models/{id}` |
| `usage.read` | `GET /credits`, `GET /usage` |
`GET /me` needs **any** valid key — it requires no specific scope. New capabilities
add new scope strings; existing keys never inherit them automatically, so an admin
edits the key's scopes or issues a new key to grant access.
## When authentication fails
Authentication and authorization failures return the standard
[error envelope](/guides/errors). A missing, malformed, unknown, expired, or revoked
key — or a key whose creator is no longer a member of the organization — returns
**`401 invalid_api_key`**:
```json 401 Unauthorized theme={null}
{
"error": {
"type": "authentication_error",
"code": "invalid_api_key",
"message": "The provided API key is invalid, expired, or revoked.",
"request_id": "req_8f14e45fceea167a"
}
}
```
A valid key that lacks the endpoint's scope returns **`403 missing_scope`**, naming
the required scope:
```json 403 Forbidden theme={null}
{
"error": {
"type": "permission_error",
"code": "missing_scope",
"message": "The API key is missing the required scope: images.generate.",
"request_id": "req_1c9a3b7d2e5f4a80",
"param": "scope"
}
}
```
Expired and unknown keys both return `401 invalid_api_key` — deliberately
indistinguishable, so an outsider cannot probe which keys once existed.
## Security best practices
Keep keys out of source control. Load them from an environment variable or a
secrets manager — never hard-code them.
```bash theme={null}
export SAMSA_API_KEY="samsa_sk_..."
```
The Samsa API is **server-side first** — CORS is deliberately restrictive and
browser calls from arbitrary origins are unsupported. A key in front-end code or
a mobile app is a leaked key. Always call the API from your backend.
There is no in-place rotation. To rotate, [create a new key](#rotating-a-key),
deploy it, then revoke the old one. Revocation is terminal and takes effect on
the very next request.
Keys can be created with an optional **expiry**. An expired key fails exactly
like an unknown one (`401 invalid_api_key`). Use expiry for temporary
integrations, trials, and contractors.
### Rotating a key
1. Create a new key in **Settings → API Keys** and copy it.
2. Deploy the new key to your integration.
3. Revoke the old key. Revocation is immediate and cannot be undone.
## Key format
A key looks like:
```
samsa_sk_gK3n8vQ1xY7bT2mW9cR4jL6hF0dS5pZaU8eN1oI3rAb
└───┬───┘└──────────────────┬─────────────────────┘
prefix 43 random base62 characters
```
The `samsa_sk_` body is 43 base62 characters encoding 256 bits of randomness. In the
app, keys are displayed as `samsa_sk_gK3n…3rAb` (a stable `prefix` plus the last four
characters) so you can identify a key without exposing it.
The constant `samsa_sk_` prefix lets secret scanners (GitHub secret scanning,
pre-commit hooks, CI checks) detect an accidentally committed Samsa key. Enable
secret scanning on your repositories so a leaked key is caught before it ships.
# Content provenance
Source: https://docs.samsa.ai/guides/content-provenance
How the Samsa API marks AI-generated and AI-modified images — C2PA signing, invisible watermarking, and the SVG scope-out — and how to verify a file.
Images the Samsa API produces are AI-generated or AI-modified content. This page
explains how those outputs are marked for provenance, how to verify a file, and the
one format the marking cannot cover.
Provenance marking of API outputs is being rolled out. The signing and
watermarking pipeline is **dark-launched** — built and staged behind a flag — and
applies to delivered raster files once it goes live. The behavior described here is
the intended state at go-live; until the flag is flipped, API-delivered files are
not yet marked.
## What gets marked
Once provenance marking is live, every **raster** image the API delivers —
generation, Magic Edit, img2img, variations, resize, upscale, and background removal
— carries two markers:
* **A C2PA manifest.** An industry-standard, cryptographically signed Content
Credentials record embedded in the file, stating that the image is AI-generated or
AI-modified and how it was produced.
* **An invisible watermark.** A durable, imperceptible mark carried in the image
itself, so provenance survives a screenshot or a metadata strip that would remove
the C2PA manifest.
### AI-generated vs AI-modified
The provenance record reflects **how the image was produced**, derived from the
operation:
| Marker | Operations |
| ---------------- | --------------------------------------------------------------------------------------------------------------- |
| **AI-generated** | Image generation from a prompt (`POST /images/generations`) |
| **AI-modified** | Operations that transform an input image — Magic Edit, img2img, variations, resize, upscale, background removal |
The visual EU AI Act marking a viewer sees is applied when an image is **downloaded
in the Samsa app** — it is a presentation-layer label, and it is **not** applied to
the files the API delivers. The API returns the asset with its embedded C2PA
manifest and watermark; adding a visible on-image label for your own end users is
your integration's choice.
## Verifying a file
The C2PA manifest is readable with any standard Content Credentials tool — nothing
Samsa-specific is required:
* **Content Credentials Verify** — the web verifier at
[contentcredentials.org/verify](https://contentcredentials.org/verify): drop in an
image to inspect its manifest.
* **`c2patool`** — the open-source [C2PA command-line tool](https://github.com/contentauth/c2pa-rs)
for reading and validating manifests in a pipeline.
Both report the manifest's claims and validate its signature, so you can confirm a
file's provenance independently of Samsa.
## SVG: an Art. 50(2) scope-out
[Vectorization](/api-reference/image-ops/vectorize) produces an **SVG**, which cannot
carry a C2PA manifest or an embedded watermark. SVG output is therefore a documented
**EU AI Act Art. 50(2) scope-out**: vector files are delivered **unsigned and
unwatermarked**.
Because that output cannot be marked, a vectorization request must acknowledge the
scope-out with `svg_acceptance: true`:
The `svg_acceptance` acknowledgment is **disclosure / audit evidence, not a
compliance waiver.** It records that the caller was told the SVG is delivered
unmarked; it does not change the underlying marking duty, which remains the
provider's and is qualified by technical feasibility.
Two independent gates apply, and both reject **before any credits are charged**:
| Gate | Requirement | Failure |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| Acknowledgment | `svg_acceptance` must be the literal boolean `true` | [`422 svg_acceptance_required`](/api-reference/image-ops/vectorize) |
| Policy acceptance | Your organization has accepted the current ToS/AUP (verified server-side — the request flag is never trusted as this fact) | `403 svg_phase1_scope_out_required` |
Accept the current Terms of Service and Acceptable Use Policy, then retry; the
server-verified acceptance record — not the request flag — is what unlocks delivery.
## See also
The SVG operation and its `svg_acceptance` request contract.
The six operations behind these outputs.
The error envelope and status codes.
What each operation costs.
# Errors
Source: https://docs.samsa.ai/guides/errors
The Samsa API error envelope, every error code, and how to handle each one.
Every error the Samsa API returns — for any endpoint, at any status — uses one
consistent JSON **envelope**. Parse the machine-readable `code`, not the human
`message` (messages may change; codes are stable).
## The error envelope
```json theme={null}
{
"error": {
"type": "authentication_error",
"code": "invalid_api_key",
"message": "The provided API key is invalid, expired, or revoked.",
"request_id": "req_8f14e45fceea167a"
}
}
```
| Field | Description |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| `error.type` | Broad category, e.g. `authentication_error`, `credit_error`, `rate_limit_error`. |
| `error.code` | Stable, specific code you branch on (the table below). |
| `error.message` | Human-readable explanation. For display and logs — do not parse it. |
| `error.request_id` | Correlation id for this request, also returned as the `X-Request-ID` response header. **Quote it when contacting support.** |
| `error.param` | Present on validation and scope errors — names the offending field or `"scope"`. |
Successful responses never contain an `error` object. Branch on the HTTP status
first, then on `error.code`.
## Error codes
| HTTP | `code` | `type` | Meaning |
| ---- | ---------------------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------- |
| 401 | `invalid_api_key` | `authentication_error` | Missing, malformed, unknown, expired, or revoked key. |
| 403 | `missing_scope` | `permission_error` | Valid key lacks the endpoint's scope. |
| 403 | `svg_phase1_scope_out_required` | `permission_error` | Vectorization only: the server-verified ToS/AUP scope-out acceptance is missing or stale. |
| 402 | `insufficient_credits` | `credit_error` | Organization credit pool is below the action cost — also the fallback when the deduction itself fails operationally. |
| 402 | `subscription_inactive` | `credit_error` | No usable organization subscription. |
| 402 | `insufficient_team_credits` | `credit_error` | The balance the API key can spend is below the action cost, and the key is assigned to a budgeted team. |
| 402 | `insufficient_unallocated_credits` | `credit_error` | The balance the API key can spend is below the action cost, and the key is not assigned to a budgeted team. |
| 404 | `not_found` | `invalid_request_error` | Unknown id, or a resource owned by another organization. |
| 422 | `validation_error` | `invalid_request_error` | Request body or parameters failed validation. |
| 422 | `svg_acceptance_required` | `invalid_request_error` | Vectorization only: `svg_acceptance` is not the literal boolean `true`. |
| 429 | `rate_limited` | `rate_limit_error` | Per-key request-rate window exceeded. |
| 429 | `too_many_active_jobs` | `rate_limit_error` | Organization's concurrent-job cap reached. |
| 500 | `internal_error` | `api_error` | Unexpected server error. |
***
### `invalid_api_key`
**HTTP 401.** The `Authorization` header is missing or malformed, or the key is
unknown, expired, or revoked — or the key creator is no longer a member of the
organization. Expired and unknown keys are intentionally indistinguishable.
```json theme={null}
{
"error": {
"type": "authentication_error",
"code": "invalid_api_key",
"message": "The provided API key is invalid, expired, or revoked.",
"request_id": "req_8f14e45fceea167a"
}
}
```
**How to handle.** Check the `Authorization: Bearer samsa_sk_…` header. If the key
was revoked or expired, [create a new one](/guides/authentication#rotating-a-key).
Do not retry — the outcome will not change.
### `missing_scope`
**HTTP 403.** The key is valid but lacks the [scope](/guides/authentication#scopes)
the endpoint requires. `param` is `"scope"` and the message names the missing scope.
```json theme={null}
{
"error": {
"type": "permission_error",
"code": "missing_scope",
"message": "The API key is missing the required scope: images.generate.",
"request_id": "req_1c9a3b7d2e5f4a80",
"param": "scope"
}
}
```
**How to handle.** An admin edits the key's scopes (or issues a new key) to grant the
named scope. Do not retry without changing the key.
### `insufficient_credits`
**HTTP 402.** The organization's spendable credit pool (subscription plus valid
top-ups) is below the action's cost. See [Pricing](/guides/pricing). If the
organization has at least one active team, the API returns the team-aware codes
[`insufficient_team_credits`](#insufficient_team_credits) or
[`insufficient_unallocated_credits`](#insufficient_unallocated_credits) instead — with
two exceptions. A budget-exempt system organization stays under the `org` regime and
keeps this generic code even with active teams. And an operational failure inside the
deduction itself (for example a degraded balance lookup) falls back to this generic
code whatever the budget regime, so a plain `insufficient_credits` under a team setup
can be transient; retry before treating it as an exhausted balance.
```json theme={null}
{
"error": {
"type": "credit_error",
"code": "insufficient_credits",
"message": "Insufficient credits available.",
"request_id": "req_2d5f4a801c9a3b7d"
}
}
```
**How to handle.** If a subsequent [`GET /credits`](/api-reference/account/credits)
shows `available` covering the cost, you **may** have hit the operational-failure
fallback — retry once. That read is a point-in-time snapshot, though: a state change
since the rejection (a top-up, refund, budget edit, or period reset) can also explain
it, so a genuine shortfall is not ruled out. Otherwise top up or upgrade the
organization's plan in the app, then retry. Nothing was charged and no job was created.
### `subscription_inactive`
**HTTP 402.** The organization has no usable subscription (none active, in grace, or
holding spendable top-ups). Distinct from `insufficient_credits`, where a subscription
exists but the pool is too low.
```json theme={null}
{
"error": {
"type": "credit_error",
"code": "subscription_inactive",
"message": "No active subscription for this organization.",
"request_id": "req_4a801c9a3b7d2d5f"
}
}
```
**How to handle.** Reactivate billing for the organization in the app, then retry.
### `insufficient_team_credits`
**HTTP 402.** Organizations can split their monthly subscription credits into
per-team **budgets**, and every API key can be assigned to a team. A key assigned to
a team with a budget spends from that team's monthly bucket; whatever is not
allocated to any team budget forms the organization's **unallocated pool**.
Purchased top-up credits are budget-exempt and remain available to cover the
remainder. This code fires when the balance the key can spend — the smaller of the
organization's remaining plan pool and the team's remaining budget, plus top-ups — is
below the action's cost. That is exactly the `available` reported by
[`GET /credits`](/api-reference/account/credits).
**The code names the budget regime, not the balance that ran out.** A key assigned to a
budgeted team gets this code even when the *organization's* plan pool, and not the team
budget, is what was exhausted — so a team can still show budget headroom. Compare
`plan_credits` with `scope.remaining` on [`GET /credits`](/api-reference/account/credits)
to see which one is binding.
```json theme={null}
{
"error": {
"type": "credit_error",
"code": "insufficient_team_credits",
"message": "Insufficient team credits for team Marketing",
"request_id": "req_6b7d2d5f4a801c9a"
}
}
```
**How to handle.** An organization admin can raise the team's monthly budget, assign
the key to a different team, or buy top-up credits (top-ups are not limited by team
budgets). Budget usage also resets with the next billing period. Nothing was charged
and no job was created.
### `insufficient_unallocated_credits`
**HTTP 402.** The key is not assigned to a budgeted team (unassigned, or its team
has no budget), so it spends from the organization's unallocated pool — the monthly
subscription credits left after subtracting the budgets of all active teams. This code
fires when the balance the key can spend — the smaller of the organization's remaining plan
pool and that unallocated pool's headroom, plus budget-exempt top-ups — is below the
action's cost, the same caveat about which balance ran out as above. Organizations
without any active team never see the two team-aware codes — they receive plain
[`insufficient_credits`](#insufficient_credits) instead.
**See it coming.** [`GET /credits`](/api-reference/account/credits) reports `available` —
what the calling key can spend right now — and a `scope` block naming its budget regime. A
job whose `estimated_credits` fits inside `available` is not refused with any of these
three codes, as long as the credit and budget/team state does not change in between —
barring the operational-failure fallback: a degraded deduction can return the generic
`insufficient_credits` even then, so retry first.
```json theme={null}
{
"error": {
"type": "credit_error",
"code": "insufficient_unallocated_credits",
"message": "Insufficient unallocated organization credits",
"request_id": "req_0a3b7d2d5f4a801c"
}
}
```
**How to handle.** An organization admin can free unallocated credits by lowering
team budgets, assign the key to a team with available budget, upgrade the plan, or
buy top-up credits. Nothing was charged and no job was created.
### `not_found`
**HTTP 404.** The id is unknown, or it belongs to another organization. The two cases
are indistinguishable by design, so existence is never disclosed across organizations.
```json theme={null}
{
"error": {
"type": "invalid_request_error",
"code": "not_found",
"message": "The requested resource was not found.",
"request_id": "req_7d2d5f4a801c9a3b"
}
}
```
**How to handle.** Verify the id, and that the key's organization owns the resource.
Do not retry.
### `validation_error`
**HTTP 422.** The request body or a parameter failed validation. `param` names the
offending field; `message` explains the constraint.
```json theme={null}
{
"error": {
"type": "invalid_request_error",
"code": "validation_error",
"message": "aspect_ratio must be one of: 1:1, 1:4, 1:8, 16:9, 2:3, 21:9, 3:2, 3:4, 4:1, 4:3, 4:5, 5:4, 8:1, 9:16.",
"request_id": "req_3b7d2d5f4a801c9a",
"param": "aspect_ratio"
}
}
```
**How to handle.** Fix the request per `message` and `param`, then resubmit. This is a
client error — retrying the same request will fail identically.
### `svg_acceptance_required`
**HTTP 422.** Only [`POST /images/vectorizations`](/api-reference/image-ops/vectorize)
returns this. SVG is a documented EU AI Act Art. 50(2) scope-out: an SVG cannot carry a
C2PA manifest or an embedded watermark, so vector outputs are delivered **unsigned**.
`svg_acceptance` must therefore be the literal boolean `true` — a missing, `false`, or
any other value is rejected with this distinct code, never the generic
[`validation_error`](#validation_error). The acknowledgment is disclosure / audit
evidence, **not** a compliance waiver. Nothing is charged and no job is created.
```json theme={null}
{
"error": {
"type": "invalid_request_error",
"code": "svg_acceptance_required",
"message": "SVG (vector) output is an EU AI Act Art. 50(2) scope-out delivered unsigned and unwatermarked. Set `svg_acceptance` to true to acknowledge this before requesting a vectorization. The acknowledgment is disclosure — it records that you were informed the output is unsigned; it is not a compliance waiver.",
"request_id": "req_3b7d2d5f4a801c9a"
}
}
```
**How to handle.** Resubmit with `"svg_acceptance": true` once your integration surfaces
the unsigned-output disclosure to whoever acts on the result.
### `svg_phase1_scope_out_required`
**HTTP 403.** Also vectorization-only, and distinct from
[`missing_scope`](#missing_scope) — the key's scope is fine. Delivery additionally
requires a **current, server-verified ToS/AUP acceptance**; the `svg_acceptance` request
flag is never trusted as that fact. At submit time a missing or stale acceptance is
refused before anything is charged. The check runs again at delivery, so a job that was
accepted can still be refused by
[`GET /images/vectorizations/{id}`](/api-reference/image-ops/get-vectorization) or on
webhook emission if the acceptance lapses in between — a delivery-time refusal adds no
charge, but it does not refund the credits the completed job already consumed.
```json theme={null}
{
"error": {
"type": "permission_error",
"code": "svg_phase1_scope_out_required",
"message": "SVG (vector) delivery requires a current, accepted Terms of Service / Acceptable Use Policy version that covers the Art. 50(2) scope-out. Your organization has not accepted the current version. Accept the latest ToS/AUP, then retry. Accepting it is disclosure that you were informed the output is unsigned; it is not a compliance waiver.",
"request_id": "req_3b7d2d5f4a801c9a"
}
}
```
**How to handle.** Accept the current ToS/AUP in the Samsa app, then retry. For a job
refused at delivery, re-request its `GET` status — the webhook is not replayed
automatically. Accepting is disclosure, not a waiver.
### `rate_limited`
**HTTP 429.** The key exceeded its per-key request-rate window. The response carries
`Retry-After` and `X-RateLimit-*` headers. See [Rate limits](/guides/rate-limits).
```json theme={null}
{
"error": {
"type": "rate_limit_error",
"code": "rate_limited",
"message": "API rate limit exceeded. Slow down and retry after the rate-limit window resets.",
"request_id": "req_5f4a801c9a3b7d2d"
}
}
```
**How to handle.** Back off and retry after the `Retry-After` interval. Use
exponential backoff for repeated 429s.
### `too_many_active_jobs`
**HTTP 429.** The organization has reached its cap on concurrent in-flight jobs. The
message includes the current count and the limit, and a `Retry-After` header is set.
```json theme={null}
{
"error": {
"type": "rate_limit_error",
"code": "too_many_active_jobs",
"message": "Too many active jobs for this organization (5/5). Wait for in-flight jobs to finish before submitting more.",
"request_id": "req_801c9a3b7d2d5f4a"
}
}
```
**How to handle.** Wait for in-flight jobs to reach a terminal status (poll their
`GET` endpoint or use a [webhook](/guides/webhooks)) before submitting more, then
retry after `Retry-After`.
### `internal_error`
**HTTP 500.** An unexpected error on Samsa's side. The response never leaks internal
details — the `request_id` is your handle for support.
```json theme={null}
{
"error": {
"type": "api_error",
"code": "internal_error",
"message": "An internal error occurred. Contact support with the request_id.",
"request_id": "req_9a3b7d2d5f4a801c"
}
}
```
**How to handle.** Retry with backoff — 500s are often transient. If it persists,
contact [support@samsa.ai](mailto:support@samsa.ai) with the `request_id`.
## Using `request_id` for support
Every error (and every success) carries a `request_id`, also returned as the
`X-Request-ID` response header. It ties your client-side error, the response header,
and Samsa's server logs to one request. Log it, and quote it when you contact
[support@samsa.ai](mailto:support@samsa.ai) — it is the fastest way for us to find
exactly what happened.
New error codes may be added over time (for example, additive per-endpoint codes).
Treat an unrecognized `code` the same as its HTTP status class, and always branch on
`code` rather than matching `message` text.
# Image inputs
Source: https://docs.samsa.ai/guides/image-inputs
How to supply source images to the API and MCP tools — by image_id, https URL, or base64 — with size, format, and URL-fetch rules.
Every operation that transforms an existing image — Magic Edit, img2img,
variations, resize, upscale, background removal, and vectorization — takes its
source image the same way. This page covers the three input modes, their limits,
and how the server fetches a URL.
## The three source modes
Over REST, each source image is **exactly one** of the following:
An image the **API key's creator** generated, edited, or uploaded in Samsa. No
upload needed; the server reads it by id. Another member's image is not readable
by your key.
A public **`https`** URL the server downloads under its SSRF guard. See
[URL fetching](#url-fetching) below.
Inline image bytes, sent as `base64` together with a `mime_type`.
Provide **one and only one** per source. Sending none, or more than one, is a
[`422 validation_error`](/guides/errors#validation_error).
```json Source examples theme={null}
// by id
{ "image_id": "8f3c…" }
// by URL
{ "url": "https://example.com/photo.jpg" }
// inline base64
{ "base64": "iVBORw0KGgo…", "mime_type": "image/png" }
```
## Size and format limits
| Rule | Value |
| ------------------------------- | ------------------------------------------------- |
| Max size per source | **10 MB** (decoded bytes) |
| Max source pixels (resize only) | **33,554,432** (32 MP, width × height) |
| Accepted formats | `image/jpeg`, `image/png`, `image/webp` |
| `mime_type` (base64) | Required, and must be one of the accepted formats |
The 10 MB cap is on the **decoded** image. For a `base64` source the encoded string
is bounded first (a longer string necessarily decodes past the limit), then the
decoded bytes are checked. Bytes that are not a readable image are rejected as a
`422` before anything is charged.
The pixel cap is independent of the size cap: a highly compressed source can sit under
10 MB and still exceed 32 MP, in which case
[`POST /images/resizes`](/api-reference/image-ops/resize) returns `422` with
`param: image` before any rendering. This 33,554,432-pixel source-raster cap is specific
to resize.
## Multiple sources (img2img)
Most operations take a single source image. **Img2img** accepts **1–14** source
images in one request — pass them as the `images` array, and source order is
preserved:
```json theme={null}
{
"prompt": "a watercolor collage",
"images": [
{ "image_id": "8f3c…" },
{ "url": "https://example.com/ref-2.png" },
{ "base64": "iVBORw0KGgo…", "mime_type": "image/webp" }
],
"resolution": "2K"
}
```
Each item follows the same one-of rule. A request with more sources than the engine
allows is a `422`.
## MCP: image\_id or image\_url only
The [MCP tools](/mcp-server) accept a source as **`image_id`** or **`image_url`**
only — there is **no `base64` input over MCP**. Fetch or reference the image by id or
public URL instead:
```json theme={null}
// MCP img2img item
{ "image_id": "8f3c…" }
// or
{ "image_url": "https://example.com/photo.jpg" }
```
This applies to every MCP tool that takes a source image: `edit_image`, `img2img`,
`create_variations`, `resize_image`, `upscale_image`, `remove_background`, and
`vectorize_image`.
## URL fetching
When you pass a `url` (REST) or `image_url` (MCP), the server downloads it under a
strict SSRF guard before using it:
* **`https` only, port 443 only.** Any other scheme or port is rejected.
* **Public addresses only.** The hostname is resolved and every resulting IP is
checked; private, loopback, link-local, and shared/CGNAT ranges are refused.
* **Redirects are re-validated.** Auto-follow is off; up to **3** redirect hops are
followed manually, and each hop is re-checked against the same guard.
* **Content-type allow-list.** Only `jpeg`, `png`, and `webp` responses are
accepted, and the 10 MB size cap is enforced on the streamed bytes (a declared
`Content-Length` is never trusted on its own).
* **Time budget.** A connect timeout of \~5 s and a soft overall wall-clock budget of
**\~30 s** span the whole download, including redirects.
Any fetch that fails a guard is a [`422 validation_error`](/guides/errors#validation_error)
naming the offending field — never a server error. Host a source somewhere publicly
reachable over `https`, or send it as `base64` (REST) instead.
## See also
What each operation costs, including the per-op table.
The same operations as tools — with the id/url-only source rule.
The error envelope and every code, including `validation_error`.
The six operations and their full request/response contracts.
# Pricing
Source: https://docs.samsa.ai/guides/pricing
How API actions draw credits from your organization's pool, with cost tables for images, edits, models, and video.
The Samsa API uses the **same credits** as the app. Every API action draws from your
organization's existing Samsa credit pool at the **same rates** you pay in the app —
there is no separate API price list and no per-seat API fee.
Credits are shared across the app and the API. An image you generate through the API
costs exactly what the same image costs in the app, and both draw down the same
organization balance. Check what a key can spend any time with
[`GET /credits`](/api-reference/account/credits).
## Image generation
Image generation costs **5 credits per output at `1K`**, scaled by resolution and
multiplied by the number of outputs:
```
credits = 5 × num_outputs × resolution_multiplier
```
| Resolution | Multiplier | Credits per output |
| ---------- | ---------- | ------------------ |
| `1K` | ×1 | 5 |
| `2K` | ×2 | 10 |
| `4K` | ×4 | 20 |
`num_outputs` defaults to **1** (the app default is 4). For example, 4 outputs at
`2K` cost `5 × 4 × 2 = 40` credits.
## Magic Edit
A Magic Edit (`POST /images/edits`) costs **5 credits** per edit at the base
resolution. When the chosen engine exposes higher-resolution tiers, the same
`1K`/`2K`/`4K` multipliers as image generation apply, and multiple outputs multiply
the cost the same way. Engines with a fixed resolution are always billed at the base
5 credits per output.
## Image operations
The six image operations — img2img, variations, resize, upscale, background
removal, and vectorization — each draw credits at submit and return the exact
amount as `estimated_credits` in the `202` (it equals what was deducted).
| Operation | Submit | Credits |
| ------------------ | ---------------------------------- | ------------------------------------------------ |
| Img2img | `POST /images/img2img` | `5 × resolution_multiplier × num_outputs` |
| Variations | `POST /images/variations` | `5 × source_resolution_multiplier × num_outputs` |
| Resize | `POST /images/resizes` | `5 × resolution_multiplier × num_outputs` |
| Upscale | `POST /images/upscales` | resolution tier × model multiplier (see below) |
| Background removal | `POST /images/background-removals` | `1` (`0` on a cache hit) |
| Vectorization | `POST /images/vectorizations` | `5` (`0` on a cache hit) |
The resolution multiplier is the same as image generation: `1K` ×1, `2K` ×2, `4K`
×4. `num_outputs` defaults to **1**. Img2img and resize take an explicit
`resolution`; **variations inherit the source image's resolution tier** (there is no
`resolution` parameter) — a source with no recoverable dimensions prices at `1K`.
### Upscale
Upscale is priced by the target resolution tier, multiplied by the model's credit
multiplier. **SeedVR** and **Crystal** are ×1; **Magnific Creative** and **Magnific
Precision** are ×3.
| Target | SeedVR / Crystal (×1) | Magnific (×3) |
| ------ | --------------------- | ------------- |
| `2K` | 5 | 15 |
| `4K` | 10 | 30 |
| `6K` | 20 | 60 |
| `8K` | 25 | 75 |
| `10K` | 35 | 105 |
| `12K` | 45 | 135 |
| `14K` | 60 † | 180 |
| `16K` | 80 † | 240 |
† For **Crystal**, `14K` and above are priced by output megapixels (see below), not
this fixed figure — the `14K`/`16K` values in the ×1 column are the SeedVR price.
Crystal matches the ×1 column exactly up to `12K`.
The Magnific column shows the tier price where the model can reach it — the
Magnific engines enforce per-model output caps, so the highest tiers are only
reachable with Crystal. Resolution classes **above `16K`** (`20K`–`38K`) are
**Crystal-only**.
For **Crystal**, resolution classes **above `12K`** (`14K` and up) are priced by the
predicted output **megapixels** instead of the tier table:
```
credits = ceil(output_megapixels × 0.6 / 5) × 5
```
A Crystal upscale whose predicted output is **101.6 MP** (a `14K`-class 16:9 image)
costs `ceil(101.6 × 0.6 / 5) × 5 = ceil(12.19) × 5 = ` **65 credits**.
### Cache hits cost nothing
Background removal and vectorization are **cached per source image**. When the source
is an `image_id` you own and a matching result already exists, the submit returns
`202` with `status: "completed"` and **`estimated_credits: 0`** immediately — no new
job runs and the [per-org concurrency cap](/guides/rate-limits) is not consumed.
Every other case (an `https` `url` or `base64` source, or an owned image with no ready
result) is the normal charged path.
A resize whose target ratio already matches the source (nothing new to outpaint)
and that has no `prompt` skips the model, returns the flattened composition, and
**refunds the unused outputs** — it still `completes`.
## Model creation
Creating a custom model (`POST /models`) costs **0 credits** — it is a billable
action recorded for your audit trail, charged at zero. You are billed for generating
*with* the model, not for creating it.
Legacy LoRA "training" is deprecated and not available through the API — the legacy
training endpoints return `410 Gone`. "Model creation" and "model training" refer to
the same Gemini-based flow; see the [overview](/api) for what you can build.
## Video generation
Video is billed at a **base of 5 credits per second**, then scaled by the engine, the
resolution, and whether audio is generated:
```
credits = round(5 × seconds × engine_multiplier × resolution_multiplier × audio_multiplier)
```
### Engine multipliers
The `engine` you pick sets the base multiplier. Some engines also generate audio, at
an additional multiplier applied on top.
| Engine | Base multiplier | Audio |
| --------------------- | --------------- | ------------ |
| `veo_3_1_lite` | ×1 | +67% (×1.67) |
| `veo_3_1` | ×5 | +25% (×1.25) |
| `veo_3_1_fast` | ×2 | +50% (×1.5) |
| `veo_3` | ×5 | +25% (×1.25) |
| `veo_3_fast` | ×2 | +50% (×1.5) |
| `veo_2` | ×4 | — |
| `kling_3_0_pro` | ×2 | +50% (×1.5) |
| `kling_2_6_pro` | ×2 | +25% (×1.25) |
| `kling_2_5_pro_turbo` | ×2 | — |
| `kling_2_0_master` | ×4 | — |
| `seedance_2_0` | ×4 | included |
| `sora_2_pro` | ×4 | included |
| `runway_gen_4` | ×2 | — |
| `hailuo_02` | ×2 | — |
| `minimax_01` | ×1 | — |
"included" means audio is generated at no extra credit cost (×1.0). A "—" means the
engine has no audio option. Engines omit `engine` to use the default,
`veo_3_1_lite`.
### Resolution multipliers
On engines that expose resolution tiers, higher resolutions cost more:
| Engine group | Resolution multipliers |
| --------------------------------------------------------- | --------------------------------------------------------------------- |
| Veo 3.x, Sora 2 Pro | `720p` ×1 · `1080p` ×2 · `4K` ×3 (where the engine supports the tier) |
| `veo_3_1_lite` | `720p` ×1 · `1080p` ×2 |
| `seedance_2_0` | `480p` ×0.5 · `720p` ×1 · `1080p` ×2.25 |
| Kling, `runway_gen_4`, `hailuo_02`, `minimax_01`, `veo_2` | Fixed resolution — no resolution multiplier |
### Worked examples
`5 × 5 × 2 = 50` credits (fixed resolution, no audio).
`5 × 8 × 1 × 2 = 80` credits (no audio).
`5 × 8 × 5 × 2 × 1.25 = 500` credits.
`5 × 5 × 1 = 25` credits (fixed resolution, no audio).
## Refunds
If a job fails on Samsa's side — a terminal provider error after credits were
deducted — the credits are **automatically refunded** to the same organization pool
they were drawn from. A `failed` job you submitted correctly does not cost you
credits. (Client errors such as `422 validation_error` are rejected before anything is
charged.)
## When you run out of credits
If the balance the key can spend cannot cover an action's cost, the submit
request returns [`402`](/guides/errors#insufficient_credits) **before** any job is
created — nothing is charged and no job row exists.
* [`insufficient_credits`](/guides/errors#insufficient_credits) — the balance is below
the cost, and the organization has no active teams (or is a budget-exempt system
organization); also the fallback when the deduction itself fails operationally,
whatever the regime — retry before treating it as a low balance. Top up or upgrade.
* [`insufficient_team_credits`](/guides/errors#insufficient_team_credits) — same, for a
key assigned to a team with its own budget. Raise the budget, reassign the key, or top up.
* [`insufficient_unallocated_credits`](/guides/errors#insufficient_unallocated_credits) —
same, for a key that is not assigned to a budgeted team. Free unallocated credits, assign
the key to a team with headroom, or top up.
* [`subscription_inactive`](/guides/errors#subscription_inactive) — the organization
has no usable subscription. Reactivate billing.
[`GET /credits`](/api-reference/account/credits) reports the balance a key can spend
(`available`) and the budget scope that determines which of the three codes it gets.
Purchase credits, add top-ups, and manage your plan in the Samsa app. API usage
draws from the same balance.
# Rate limits
Source: https://docs.samsa.ai/guides/rate-limits
Per-key request limits, per-organization concurrency caps, the headers to watch, and how to back off.
The Samsa API protects shared capacity with two independent limits: a **per-key
request rate** and a **per-organization concurrent-job cap**. Both return
[`429`](/guides/errors#rate_limited) with headers that tell you when to retry.
These limits are defaults and are **subject to change**. If your integration needs
a higher ceiling, contact [support@samsa.ai](mailto:support@samsa.ai).
## Per-key request rate
Each API key may make up to **60 requests per minute**, measured as a sliding
60-second window. Exceeding it returns `429` with `code: "rate_limited"`.
Successful responses and rate-limit `429`s carry the current window state (the
`Retry-After` header is added on `429`s only):
```
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 41
X-RateLimit-Reset: 1782043260
```
| Header | Meaning |
| ----------------------- | ------------------------------------------------------------------ |
| `X-RateLimit-Limit` | Requests allowed per window (default `60`). |
| `X-RateLimit-Remaining` | Requests left in the current window. |
| `X-RateLimit-Reset` | Unix timestamp (seconds) when the window resets. |
| `Retry-After` | Seconds to wait before retrying. **Sent on `429` responses only.** |
Read `X-RateLimit-Remaining` on successful responses to slow down *before* you hit
the limit.
## Per-organization concurrency cap
Independently of request rate, an organization may have at most **5 concurrent
in-flight jobs** — generation, edit, video, and model-creation jobs that are still
`pending` or `processing`, counted across every key in the organization. Submitting
another job while at the cap returns `429` with `code: "too_many_active_jobs"` and a
`Retry-After` header:
```json 429 Too Many Requests theme={null}
{
"error": {
"type": "rate_limit_error",
"code": "too_many_active_jobs",
"message": "Too many active jobs for this organization (5/5). Wait for in-flight jobs to finish before submitting more.",
"request_id": "req_801c9a3b7d2d5f4a"
}
}
```
This cap protects processing capacity, so it is keyed to the whole organization, not a
single key. Wait for in-flight jobs to reach a terminal status — poll their `GET`
endpoint, or subscribe to a [webhook](/guides/webhooks) — before submitting more.
## An example 429
A `429` from the per-key window includes both the `Retry-After` and `X-RateLimit-*`
headers:
```http theme={null}
HTTP/1.1 429 Too Many Requests
Retry-After: 12
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1782043260
Content-Type: application/json
{
"error": {
"type": "rate_limit_error",
"code": "rate_limited",
"message": "API rate limit exceeded. Slow down and retry after the rate-limit window resets.",
"request_id": "req_5f4a801c9a3b7d2d"
}
}
```
## Handling 429s
When a `429` includes a `Retry-After` header, wait at least that many seconds
before retrying. It is the authoritative signal.
For repeated `429`s, increase the delay between attempts (for example
1s, 2s, 4s, 8s…), capped at a sensible maximum, with a little random jitter to
avoid thundering-herd retries.
Watch `X-RateLimit-Remaining` and throttle client-side before you hit `0`. For
the concurrency cap, bound how many jobs you keep in flight at once.
Below is a minimal retry loop that honors `Retry-After` and falls back to exponential
backoff.
```python Python theme={null}
import time
import requests
def request_with_retry(method, url, *, headers, max_retries=5, **kwargs):
for attempt in range(max_retries + 1):
resp = requests.request(method, url, headers=headers, **kwargs)
if resp.status_code != 429 or attempt == max_retries:
return resp
retry_after = resp.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2**attempt
time.sleep(delay)
return resp
```
```typescript TypeScript theme={null}
async function requestWithRetry(
url: string,
init: RequestInit,
maxRetries = 5,
): Promise {
for (let attempt = 0; ; attempt++) {
const resp = await fetch(url, init);
if (resp.status !== 429 || attempt === maxRetries) return resp;
const retryAfter = resp.headers.get("Retry-After");
const delayMs = retryAfter ? Number(retryAfter) * 1000 : 2 ** attempt * 1000;
await new Promise((r) => setTimeout(r, delayMs));
}
}
```
## Outer per-IP protection
Beyond these per-key and per-organization limits, Samsa applies a coarse **per-IP**
protection layer at the network edge (shared with the rest of the platform). It is a
backstop against abusive traffic, not a limit you tune per integration — well-behaved
server-side clients that respect the limits above will not encounter it. If you route
many organizations through a single egress IP and see unexpected throttling, contact
[support@samsa.ai](mailto:support@samsa.ai).
# Webhooks
Source: https://docs.samsa.ai/guides/webhooks
Receive a signed callback when a job finishes, verify the signature, and handle retries.
Every generation endpoint is asynchronous. Instead of polling the `GET` endpoint, you
can pass a **`webhook_url`** and Samsa will `POST` a signed event to it when the job is
`completed` or `failed`.
## Requesting a webhook
Add `webhook_url` to any generation request. It is **per request** — different jobs
can target different URLs.
```bash theme={null}
curl -X POST https://api.samsa.ai/public/v1/images/generations \
-H "Authorization: Bearer $SAMSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "A ceramic mug on linen, soft daylight",
"webhook_url": "https://api.example.com/hooks/samsa"
}'
```
Webhooks are a **convenience, not the source of truth**. If every delivery attempt
fails, the job result is still available from its `GET` endpoint. Poll as a fallback
for anything critical.
## Events
Webhooks fire only on **terminal** transitions — `completed` or `failed`. Jobs that
are `cancelled` do **not** emit a webhook.
| Event | Fires when |
| ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
| `image.generation.completed` / `image.generation.failed` | An image generation completes or fails. |
| `image.edit.completed` / `image.edit.failed` | A Magic Edit completes or fails. |
| `image.img2img.completed` / `image.img2img.failed` | An [img2img](/api-reference/image-ops/get-img2img) job completes or fails. |
| `image.variation.completed` / `image.variation.failed` | A [variations](/api-reference/image-ops/get-variations) job completes or fails. |
| `image.resize.completed` / `image.resize.failed` | A [resize](/api-reference/image-ops/get-resize) job completes or fails. |
| `image.upscale.completed` / `image.upscale.failed` | An [upscale](/api-reference/image-ops/get-upscale) job completes or fails. |
| `image.background_removal.completed` / `image.background_removal.failed` | A [background-removal](/api-reference/image-ops/get-background-removal) job completes or fails. |
| `image.vectorize.completed` / `image.vectorize.failed` | A [vectorization](/api-reference/image-ops/get-vectorization) job completes or fails. |
| `video.generation.completed` / `video.generation.failed` | A video generation completes or fails. |
| `model.completed` / `model.failed` | A model creation completes or fails. |
## Payload
The request body is JSON. The `data` object is the same shape you get from the job's
`GET` status endpoint.
```json theme={null}
{
"event": "image.generation.completed",
"id": "7f9c0e2a-1b3d-4c5e-8f6a-9b0c1d2e3f40",
"status": "completed",
"organization_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"api_key_id": "9c8d7e6f-5a4b-4c3d-2e1f-0a9b8c7d6e5f",
"created_at": "2026-07-02T12:00:00Z",
"data": {
"id": "7f9c0e2a-1b3d-4c5e-8f6a-9b0c1d2e3f40",
"status": "completed",
"created_at": "2026-07-02T12:00:00Z",
"credits_used": 5,
"images": [
{
"id": "d4c3b2a1-6f5e-4b3a-9d8c-1e0f2a3b4c5d",
"url": "https://cdn.samsa.ai/user-.../7f9c0e2a.png?X-Amz-Signature=...",
"width": 1024,
"height": 1024,
"seed": 128390
}
],
"error": null
}
}
```
For a `failed` event, `status` is `"failed"` and `data` carries an `error` object
using the same inner shape as the [error envelope](/guides/errors#the-error-envelope).
## Signature headers
Every delivery carries three headers:
```
webhook-id: msg_2n0jJ2mCw4T5qX1aB3cD4e
webhook-timestamp: 1782043200
webhook-signature: v1,F9epTMwALBtPM7ghiLNEmdmVN7TizCpZ+zAHLPwip9A=
```
| Header | Description |
| ------------------- | -------------------------------------------------------------------------------------------------- |
| `webhook-id` | Unique id for the event, **stable across retries** (use it to deduplicate). |
| `webhook-timestamp` | Unix seconds when the event was sent. Reject if it is more than 300s from now (replay protection). |
| `webhook-signature` | Space-separated list of `v1,` signatures. Verify against **each** `v1,` candidate. |
The scheme is Svix-compatible: HMAC-SHA256 over
`{webhook-id}.{webhook-timestamp}.{raw-body}`, base64-encoded, prefixed with `v1,`.
```
signed_input = utf8(f"{webhook_id}.{webhook_timestamp}.") + raw_body_bytes
key = base64_decode(webhook_secret without the "whsec_" prefix)
signature = "v1," + base64( HMAC_SHA256(key, signed_input) )
```
Sign and verify over the **raw request bytes** exactly as received — never a
re-serialized copy. Re-serializing (re-ordering keys, changing whitespace) changes
the bytes and breaks the signature.
## Verify the signature
Your per-key `webhook_secret` lives in the app under **Settings → API Keys** (each key
has its own secret; rotate it independently of the key). The snippets below are
verified against a **fixed test vector** — run them as-is and they return `true`, so
you can confirm your implementation reproduces the signature byte-for-byte before
wiring in a real secret.
The test vector's body is a fixed reference string, so its exact bytes never change
and the signature stays reproducible — it is intentionally not the live event
payload shown [above](#payload). Signature verification always runs over the **exact
raw bytes you receive**, whatever their shape, so this is purely a self-test for your
verifier. Likewise, `webhook-id` is an opaque, stable-per-event string — treat it as
a token, never parse it.
```python Python theme={null}
import base64
import hashlib
import hmac
def verify(secret: str, webhook_id: str, timestamp: int, body: bytes, header: str) -> bool:
# Accept both base64 alphabets + missing padding (real secrets are urlsafe/unpadded).
token = secret.removeprefix("whsec_").replace("+", "-").replace("/", "_")
key = base64.urlsafe_b64decode(token + "=" * (-len(token) % 4))
signed_input = f"{webhook_id}.{timestamp}.".encode() + body
expected = base64.b64encode(
hmac.new(key, signed_input, hashlib.sha256).digest(),
).decode("ascii")
valid = False
for candidate in header.split(" "):
if candidate.startswith("v1,"):
valid = hmac.compare_digest(candidate[3:], expected) or valid
return valid
# Fixed test vector — any correct implementation reproduces this signature.
assert verify(
"whsec_c2FtcGxlLXNlY3JldC1kby1ub3QtdXNl",
"msg_2n0jJ2mCw4T5qX1aB3cD4e",
1782043200,
b'{"type":"image.generation.completed","created_at":"2026-07-02T12:00:00Z",'
b'"data":{"id":"7f9c0e2a-1b3d-4c5e-8f6a-9b0c1d2e3f40","object":"image.generation",'
b'"status":"completed"}}',
"v1,F9epTMwALBtPM7ghiLNEmdmVN7TizCpZ+zAHLPwip9A=",
)
```
```typescript TypeScript theme={null}
import crypto from "node:crypto";
function verify(
secret: string,
webhookId: string,
timestamp: number,
body: Buffer,
header: string,
): boolean {
// Accept both base64 alphabets + missing padding (real secrets are urlsafe/unpadded).
const token = secret.replace(/^whsec_/, "").replace(/-/g, "+").replace(/_/g, "/");
const key = Buffer.from(token, "base64");
const signedInput = Buffer.concat([
Buffer.from(`${webhookId}.${timestamp}.`, "utf8"),
body,
]);
const expected = crypto.createHmac("sha256", key).update(signedInput).digest("base64");
let valid = false;
for (const candidate of header.split(" ")) {
if (!candidate.startsWith("v1,")) continue;
const sig = Buffer.from(candidate.slice(3));
const exp = Buffer.from(expected);
if (sig.length === exp.length && crypto.timingSafeEqual(sig, exp)) valid = true;
}
return valid;
}
// Fixed test vector — any correct implementation reproduces this signature.
const ok = verify(
"whsec_c2FtcGxlLXNlY3JldC1kby1ub3QtdXNl",
"msg_2n0jJ2mCw4T5qX1aB3cD4e",
1782043200,
Buffer.from(
'{"type":"image.generation.completed","created_at":"2026-07-02T12:00:00Z",' +
'"data":{"id":"7f9c0e2a-1b3d-4c5e-8f6a-9b0c1d2e3f40","object":"image.generation",' +
'"status":"completed"}}',
"utf8",
),
"v1,F9epTMwALBtPM7ghiLNEmdmVN7TizCpZ+zAHLPwip9A=",
);
if (!ok) throw new Error("signature verification failed");
```
In production, always also enforce the **timestamp window**: reject the delivery if
`|now − webhook-timestamp| > 300` seconds.
## Retries and delivery rules
* **Success** is any `2xx` response returned within a **10-second** timeout (connect
timeout 5s). A `3xx` is treated as a failure — Samsa does **not** follow redirects.
* On failure, Samsa retries with the initial attempt plus **5 retries** — **6 delivery
attempts total** — backing off `5s → 30s → 2m → 15m → 1h`.
* After the final attempt the delivery is dropped (and logged). The job result stays
queryable from its `GET` endpoint.
* Endpoints must be **HTTPS**. Respond quickly (`2xx`) and do heavy processing
asynchronously so you never exceed the 10-second window.
Use the stable `webhook-id` to make your handler **idempotent**. A retried delivery
reuses the same `webhook-id`, so you can safely ignore an event you have already
processed.
# Welcome to the Samsa API
Source: https://docs.samsa.ai/index
Samsa's image and video studio, available to your own code — over REST, from any MCP client, or as a free content check.
Your first image in five steps
Create an organization API key, submit a generation, and poll it to a
finished asset — with copy-paste curl, Python, and TypeScript.
## Three ways in
Generate, edit, transform, and train over plain HTTP.
Drive the same tools from Claude, ChatGPT, Cursor, or n8n — one URL, nothing
to install.
Check whether an image or video was made with Samsa. Free, no key needed.
## What you can build
Prompt to image, composed with your organization's trained models and color
palettes.
Edit an existing image from a prompt, with or without a mask.
img2img, variations, resize, upscale, background removal, and vectorization.
Video from a start frame, from text, or from text styled with your models.
Train style, object, person, and setting models from a handful of images.
Check a key, the credit balance it can spend, and your organization's usage.
## Before you build
Organization keys, scopes, and the Bearer header every request needs.
What each action costs in credits, and what happens when the pool runs out.
Per-key request rate, per-organization concurrency, and how to back off.
The error envelope, every code you can receive, and how to handle it.
How to supply source images — by `image_id`, https URL, or base64.
New endpoints and MCP tools, newest first.
Every generation endpoint is **asynchronous**: a `POST` returns `202 Accepted`
with a job `id`, you poll the matching `GET` — or supply a `webhook_url` — and
a completed job carries presigned URLs to the finished assets.
# MCP server
Source: https://docs.samsa.ai/mcp-server
Connect Samsa to Claude, ChatGPT, Microsoft Copilot, and any MCP client: images with your trained models, Magic Edit, and video.
Samsa runs a **remote Model Context Protocol (MCP) server**, so any MCP-capable
client — Claude, ChatGPT, Microsoft Copilot, Cursor, n8n, and more — drives your
organization's Samsa studio as a set of tools. Generate images with your trained
**style**, **object**, **person**, and **setting** models, run **Magic Edit**,
**transform** and **upscale** images, produce **video**, and check your **credit
balance** — all from inside the app or agent you already work in. Nothing to install:
it's one URL and a sign-in.
Turn a prompt into images, optionally composing your organization's trained
**style**, **object**, **person**, and **setting** models and color palettes.
Edit an existing image from a prompt — with or without a mask — and reuse the
same trained models for on-brand results.
Run img2img, generate variations, resize by outpainting, upscale to high
resolution, remove a background, or vectorize to SVG — one tool per operation.
Produce video from a start frame, from text, or from text styled with your
trained models — all as a single tool call.
Poll any job to completion and read your organization's remaining credit
balance — reads are always free.
**Server URL** — add this one endpoint to any MCP client:
```
https://api.samsa.ai/mcp
```
It is a **remote** server over **Streamable HTTP** — there is nothing to install,
no local process to run, and a single path (`/mcp`, no trailing slash) serves
both authentication modes. The transport is stateless: every tool call returns a
single JSON response, and generation tools return a job id immediately so nothing
holds a long-lived stream open.
## Get connected in three steps
Interactive apps — **Claude**, **ChatGPT**, and **Microsoft Copilot**
(through Copilot Studio) — sign in with **OAuth**; you approve a consent screen
in the Samsa app and never paste a key. Headless clients — **Claude Code**,
**Cursor**, **n8n**, SDKs — use an [API key](/guides/authentication) created in
**Settings → API Keys**.
Point your client at `https://api.samsa.ai/mcp`. There's nothing to install
and no local process — see [Connect your client](/mcp-server/clients) for
the exact one-time setup.
Your client lists the **fifteen Samsa tools**. Ask it to generate an image,
run a Magic Edit, transform or upscale an image, make a video, or create and
update your trained models — it submits each job and polls the async ones to
completion for you.
## Authentication
The MCP endpoint accepts **two credential types on the same URL**. Pick the one
that matches your client:
| Mode | Best for | How it works |
| -------------------------- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **OAuth 2.1** | Interactive apps — **Claude** (web/desktop), **ChatGPT**, **Microsoft Copilot** (via Copilot Studio) | You sign in with your Samsa account and approve a consent screen in the app. The client handles the token; you never paste a key. |
| **API key** (`samsa_sk_…`) | Headless clients — **Claude Code**, **Cursor**, **VS Code**, **n8n**, SDKs | Send `Authorization: Bearer samsa_sk_…`. Create keys in **Settings → API Keys**; the key's [scopes](/guides/authentication#scopes) gate which tools it can call. |
Both act for an **organization**: credits are drawn from that organization's pool
and generated assets appear in the app under the connected account. See
[Authentication](/guides/authentication) for how keys, scopes, and organizations
work.
OAuth sign-in follows the standard MCP flow: the client discovers Samsa's
authorization server from the `401` challenge, registers itself dynamically
(PKCE, no client secret), and sends you through a consent screen before
exchanging a short-lived access token. If a sign-in doesn't complete, tell us
at [support@samsa.ai](mailto:support@samsa.ai).
## The async pattern
The nine credit-costing media tools are **asynchronous** — they enqueue a job and
return right away, so your client never blocks waiting for a render. `create_model`
is a tenth async submit tool (poll `get_job_status(kind="model", …)`) but it is free.
Call any submit tool — `generate_image`, `edit_image`, `img2img`,
`create_variations`, `resize_image`, `upscale_image`, `remove_background`,
`vectorize_image`, or `generate_video`. It returns
`{ id, status: "pending", estimated_credits, next_step }` in milliseconds, and
the estimated credits are deducted from your organization's pool at submit.
Call `get_job_status(kind=…, id=…)` with the id you received and the `kind` the
submit tool named. The status moves `pending` → `processing` → `completed`
(or `failed`). Image jobs typically finish in 30 seconds to two minutes; video
in one to five.
Once `completed`, the response carries presigned result URLs valid for 24
hours. If a job ends `failed` on Samsa's side, the credits are automatically
[refunded](/guides/pricing#refunds) to the same pool.
`remove_background` and `vectorize_image` are **cached** per source image: when the
source is an `image_id` you own that already has a result, the call skips the queue
and returns `{ status: "completed", estimated_credits: 0 }` immediately — no credits
and no concurrency slot consumed.
The server tells connected models this itself: every submit result includes a
`next_step` string with the exact `get_job_status` call to make, so a capable
agent polls without extra prompting.
## Read next
The fifteen tools, their parameters, scopes, and what each costs.
One-time setup for Claude, ChatGPT, Cursor, VS Code, n8n, and more.
Security and credits, and how to read a rejected call.
## See also
Organization keys, scopes, and the Bearer header.
How image, edit, and video credits are calculated.
Per-key rate, per-org concurrency, and back-off.
The REST surface behind the same tools.
# Connect your client
Source: https://docs.samsa.ai/mcp-server/clients
One-time setup for Claude, ChatGPT, Microsoft Copilot, Claude Code, Cursor, VS Code, n8n, and more.
Add `https://api.samsa.ai/mcp` to your client below. **Claude**, **ChatGPT**, and
**Microsoft Copilot** can sign in with OAuth; the remaining clients authenticate
with an [API key](/guides/authentication) (`Authorization: Bearer samsa_sk_…`).
The API-key snippets below show the key inline for readability. In any config
that is committed or shared, **don't store a real key** — use your client's
environment-variable interpolation (shown for Claude Code, Cursor, and VS Code)
or keep the config user-level. A leaked `samsa_sk_…` should be
[revoked](/guides/authentication#rotating-a-key) immediately.
Claude (web and desktop) connects to remote MCP servers as a **custom
connector**:
Open **Settings → Connectors → Add custom connector** and set the URL to
`https://api.samsa.ai/mcp`.
Claude opens Samsa's **OAuth sign-in**; sign in and approve the consent
screen. Claude's tools list then shows the Samsa tools.
Exact menu labels vary by version — the essentials are the custom-connector
flow and the Samsa server URL. Claude web calls `/mcp` from the browser with
`Origin: https://claude.ai`, which Samsa allows, so discovery and sign-in work
without extra configuration. On desktop, follow Anthropic's current connector
instructions and use the same server URL.
In ChatGPT, open **Settings → Apps & Connectors** and enable **developer
mode**.
Add an app / MCP connection with the URL `https://api.samsa.ai/mcp`.
ChatGPT runs the **OAuth 2.1** sign-in and consent; approve it to expose
the Samsa tools.
Exact menu labels vary by version — the essentials are enabling developer mode
and adding the Samsa server URL. Custom MCP connections require a ChatGPT plan
that includes developer mode / connectors; availability changes over time, so
check your plan if the option is missing. ChatGPT calls `/mcp` with
`Origin: https://chatgpt.com` (or `https://chat.openai.com`), both of which
Samsa allows.
Microsoft connects MCP servers through **Copilot Studio**. Add Samsa there
once, then publish the agent to the **Microsoft 365 Copilot and Teams**
channel so your organization uses it from Copilot chat and Teams.
On your agent's **Tools** page, select **Add a tool → New tool → Model
Context Protocol**. Set **Server URL** to `https://api.samsa.ai/mcp` and
give the server a name and description.
The agent's orchestrator reads that description to decide when to call
Samsa, so be concrete — for example, "Generates and edits images and video
using the organization's trained Samsa models."
Samsa works with either option the wizard offers:
* **OAuth 2.0 → Dynamic discovery** — Samsa supports dynamic client
registration with discovery, so Copilot Studio finds the endpoints and
registers itself. Each user signs in with their own Samsa account and no
key is shared.
* **API key** — set **Type** to **Header** and the header name to
`Authorization`. The connection value is `Bearer samsa_sk_…`.
Select **Create**, then **Create a new connection**, then **Add to agent**.
Publish the agent and, under **Turn on Microsoft 365**, select **Make agent
available in Microsoft 365 Copilot**. Users then reach Samsa from the
Microsoft 365 Copilot app and Teams.
Copilot Studio supports the **Streamable** transport, which is what Samsa
serves. Access to MCP servers runs through Power Platform connectors, so any
data policy governing those also governs Samsa's tools.
Microsoft 365 Copilot's **federated connectors** are a different feature,
scoped to read-only data retrieval. Samsa's generation tools are added
through Copilot Studio as above.
**API key · verified**
Add the server with the HTTP transport and a Bearer header:
```bash theme={null}
claude mcp add --transport http samsa https://api.samsa.ai/mcp \
--header "Authorization: Bearer samsa_sk_..."
```
By default this registers the server for your own use (local scope). To share
it with your team, add `--scope project` and Claude Code writes a project
`.mcp.json`. Because that file is committed, keep the key out of it — Claude
Code expands environment variables in `headers`:
```json theme={null}
{
"mcpServers": {
"samsa": {
"type": "http",
"url": "https://api.samsa.ai/mcp",
"headers": { "Authorization": "Bearer ${SAMSA_API_KEY}" }
}
}
}
```
Confirm the connection with `claude mcp get samsa` — it should report
**Connected**.
Prefer OAuth? Run `claude mcp add --transport http samsa
https://api.samsa.ai/mcp` **without** the header and complete the OAuth
sign-in on first use.
Add to `~/.cursor/mcp.json` (global) or project `.cursor/mcp.json`. Cursor
interpolates environment variables in `headers`, so reference the key rather
than inlining it in a shared file:
```json theme={null}
{
"mcpServers": {
"samsa": {
"url": "https://api.samsa.ai/mcp",
"headers": { "Authorization": "Bearer ${env:SAMSA_API_KEY}" }
}
}
}
```
Add to `.vscode/mcp.json`:
```json theme={null}
{
"servers": {
"samsa": {
"type": "http",
"url": "https://api.samsa.ai/mcp",
"headers": { "Authorization": "Bearer samsa_sk_..." }
}
}
}
```
Don't commit a real key in a workspace file. VS Code supports
`${input:...}` variables and user-level MCP config — use one of those so the
secret isn't checked in with your project.
Add to `~/.codeium/windsurf/mcp_config.json`:
```json theme={null}
{
"mcpServers": {
"samsa": {
"serverUrl": "https://api.samsa.ai/mcp",
"headers": { "Authorization": "Bearer samsa_sk_..." }
}
}
}
```
Add to `~/.codex/config.toml`:
```toml theme={null}
[mcp_servers.samsa]
url = "https://api.samsa.ai/mcp"
http_headers = { Authorization = "Bearer samsa_sk_..." }
```
In the **MCP Client** node, set:
* **Endpoint** — `https://api.samsa.ai/mcp`
* **Transport** — `HTTP Streamable`
* **Authentication** — Header Auth with `Authorization: Bearer samsa_sk_...`
n8n runs server-side (no browser `Origin` header), so it connects without
any extra configuration.
Add an HTTP MCP server to `~/.gemini/settings.json`. Config key names
differ across Gemini CLI versions — confirm against the current Gemini CLI
MCP docs — but the shape is the Samsa URL plus a Bearer header:
```json theme={null}
{
"mcpServers": {
"samsa": {
"httpUrl": "https://api.samsa.ai/mcp",
"headers": { "Authorization": "Bearer samsa_sk_..." }
}
}
}
```
To verify the server from a neutral tool, use the
[MCP Inspector](https://github.com/modelcontextprotocol/inspector). Drive
it from the GUI (or a config file) with the Streamable-HTTP transport:
```json theme={null}
{
"mcpServers": {
"samsa": {
"type": "streamable-http",
"url": "https://api.samsa.ai/mcp",
"headers": { "Authorization": "Bearer samsa_sk_..." }
}
}
}
```
The Inspector uses the same Streamable-HTTP transport Samsa verifies
against, and runs on `http://localhost:6274`, which Samsa allows. Use it to
walk the handshake, list the fifteen tools, and make a test call.
Config snippets for Claude Code, Cursor, VS Code, Windsurf, Codex, n8n, Claude
web, ChatGPT, and the MCP Inspector come from Samsa's backend client-config
verification matrix. If a step is off, email
[support@samsa.ai](mailto:support@samsa.ai) and we'll fix it fast.
# MCP tools
Source: https://docs.samsa.ai/mcp-server/tools
Every tool the Samsa MCP server exposes, with its parameters, required scope, and credit cost.
The server exposes **fifteen tools**. Six are free — the four reads (`list_models`,
`get_model`, `get_job_status`, `get_credit_balance`) and the two model-management
tools (`create_model`, `update_model`). The other **nine** cost
[credits](/guides/pricing) from your organization's pool, at the same rates as the
app and REST API.
| Tool | What it does | Scope | Cost |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | ---------------------------------------- |
| `list_models` | List your organization's ready trained models (id, name, category, thumbnail). | `models.read` | Free |
| `get_model` | Full detail for one model — status, reference images, default prompt, trigger words. | `models.read` | Free |
| `create_model` | Train a new model from 1–10 reference images (async). | `models.write` | Free |
| `update_model` | Update a model's name, default prompt, and/or always-applied instruction. | `models.write` | Free |
| `generate_image` | Generate images from a prompt, optionally composing your trained models. | `images.generate` | 5 × outputs × resolution |
| `edit_image` | Magic Edit — edit an image from a prompt. | `images.edit` | 5 per output |
| `img2img` | Transform 1–14 reference images with a prompt. | `images.edit` | 5 × outputs × resolution |
| `create_variations` | Generate creative variations of one image. | `images.transform` | 5 × outputs × source resolution |
| `resize_image` | Resize to a new aspect ratio by outpainting. | `images.transform` | 5 × outputs × resolution |
| `upscale_image` | Upscale one image to a higher resolution (four models). | `images.transform` | Tier × model multiplier |
| `remove_background` | Remove the background (transparent PNG). | `images.transform` | 1 (0 on a cache hit) |
| `vectorize_image` | Vectorize one image to SVG. | `images.transform` | 5 (0 on a cache hit) |
| `generate_video` | Generate video from a start frame, from text, or from text styled with your models. | `videos.generate` | 5 / second × engine × resolution × audio |
| `get_job_status` | Poll a submitted image, transform, video, or model job for status and results. | scope of the submitting tool | Free |
| `get_credit_balance` | Read the credit balance this connection can spend, the organization-wide totals, its budget scope, and the billing period. | `usage.read` | Free |
A tool call rejected for a missing scope returns a structured tool error (not a
crash) naming the scope it needs. Keys are created with all scopes by default;
an admin can narrow or widen a key in **Settings → API Keys**.
## Parameters
* `category` *(optional)* — filter to one of `style`, `object`, `person`,
`setting`.
Returns your organization's **ready** models with `id`, `name`, `category`,
and a presigned `thumbnail_url`. Use the ids or names as `style_id` /
`object_ids` / `person_ids` / `setting_ids` in `generate_image` and
`generate_video` — a name resolves to a model visible to you.
* `model_id` *(required)* — the model's id.
Returns full detail: status, readiness, presigned reference-image URLs, the
default prompt, and trigger words.
* `name` *(required)* — the model name.
* `category` *(required)* — one of `style`, `object`, `person`, `setting`.
* `images` *(required)* — 1–10 reference images. Each is **either** a public
`https` URL **or** an inline base64 data URI (`data:image/png;base64,…`) —
`image/jpeg`, `image/png`, or `image/webp`, ≤ 10 MB each.
* `instruction` *(optional)* — the model's always-applied guidance (≤ 8000
chars), injected as mandatory ("MUST FOLLOW") direction into every
generation that composes the model. See
[Model Training](/api-reference/model-training/overview).
* `webhook_url` *(optional)* — an `https` URL notified once when training
reaches a terminal status.
**Async** — returns `{ id, status: "pending", estimated_credits }`
immediately; poll `get_job_status(kind="model", id=…)` (which also needs the
`models.read` scope) until `completed` or `failed`, then use the model id in
`generate_image` / `generate_video`.
**Free** — model creation deducts no credits. Large-file presigned uploads are
REST-only (not exposed over MCP) — use
[`POST /models/prepare`](/api-reference/model-training/prepare) for those.
* `model_id` *(required)* — the model to update.
* `name` *(optional)* — a new model name.
* `default_prompt` *(optional)* — a new default prompt.
* `instruction` *(optional)* — the model's always-applied guidance (≤ 8000
chars). Send an empty string to clear it.
Supply **at least one** of `name`, `default_prompt`, or `instruction`; omitted
fields are left unchanged. **Synchronous** — returns the full updated model
immediately (same shape as `get_model`). **Free.**
* `prompt` *(required)* — the text prompt.
* `style_id`, `object_ids`, `person_ids`, `setting_ids` *(optional)* — trained
model ids or names from `list_models` to compose (a name resolves to a
model visible to you).
* `color_palette` *(optional)* — a color palette to apply, given as its **name
or id** (same as the trained-model refs).
* `num_outputs` (`1`–`4`, default `1`), `aspect_ratio` (default `"1:1"`),
`resolution` (default `"1K"`; `1K` / `2K` / `4K`).
`aspect_ratio` accepts `1:1` (default), `2:3`, `3:2`, `3:4`, `4:3`, `4:5`,
`5:4`, `9:16`, `16:9`, `21:9`, `1:4`, `4:1`, `1:8`, and `8:1`.
**Async** — returns `{ id, status: "pending", estimated_credits }`
immediately. Cost: `5 × num_outputs × resolution` (`1K` ×1, `2K` ×2, `4K` ×4),
deducted at submit.
* `prompt` *(required)* — how to edit the image.
* Exactly **one** of `image_id` (an image in your Samsa context) or
`image_url` (a public https URL).
* `style_id`, `object_ids`, `person_ids`, `setting_ids` *(optional)* — trained
model ids or names from `list_models` to reuse for on-brand edits (a name
resolves to a model visible to you), at parity with `generate_image`.
* `color_palette` *(optional)* — a color palette to apply, given as its **name
or id**.
* `engine` *(optional)* — `nano_banana_pro` (default), `gemini`, or `kontext`.
Supplying any trained-model ref or a color palette forces `nano_banana_pro`.
**Async** — returns `{ id, status: "pending", estimated_credits }`. Cost: 5
credits per output (`nano_banana_pro` scales with resolution: `1K` ×1, `2K`
×2, `4K` ×4), deducted at submit.
* `prompt` *(required)* — how to transform the sources.
* `images` *(required)* — **1–14** sources; each item is **either**
`{ image_id }` **or** `{ image_url }` (an https URL). Source order is
preserved. No base64 over MCP — see [Image inputs](/guides/image-inputs).
* `engine` *(optional)* — `nano_banana_pro` (default) or `nano_banana_2`.
* `aspect_ratio` *(optional)* — validated against the engine's list
(`nano_banana_2` additionally allows `4:1`, `1:4`, `8:1`, `1:8`); omitted
preserves the source shape.
* `resolution` (`1K` default, `2K`, `4K`), `num_outputs` (default `1`).
**Async** — poll `get_job_status(kind="img2img", id=…)`. Cost:
`5 × num_outputs × resolution` (`1K` ×1, `2K` ×2, `4K` ×4). Scope
`images.edit`.
* **One** of `image_id` or `image_url` *(required)*.
* `target` *(optional)* — what may change: `everything` (default), `person`,
`object`, `scene`.
* `creativity` *(optional)* — `subtle` or `creative` (default).
* `variation_instructions` / `preservation_instructions` *(optional)* —
free text, ≤ 2000 chars each.
* `num_outputs` (`1`–`4`, default `1`).
The output **resolution is inherited from the source** (no `resolution`
argument). **Async** — poll `get_job_status(kind="image_variation", id=…)`.
Cost: `5 × num_outputs × source resolution` (`1K` ×1, `2K` ×2, `4K` ×4; a
source with no recoverable dimensions prices at `1K`). Scope
`images.transform`.
* `aspect_ratio` *(required)* — one of `21:9`, `16:9`, `3:2`, `4:3`, `5:4`,
`1:1`, `4:5`, `3:4`, `2:3`, `9:16`.
* **One** of `image_id` or `image_url` *(required)*.
* `resolution` (`1K` default, `2K`, `4K`), `num_outputs` (`1`–`4`, default
`1`).
* `prompt` *(optional)* — guidance for the newly exposed area.
* `placement` *(optional)* — `{ gravity, scale }` positioning the source on the
canvas (`gravity` one of `center` \[default], `top`, `bottom`, `left`,
`right`, `top_left`, `top_right`, `bottom_left`, `bottom_right`; `scale` in
`(0, 1]`).
Resizes by outpainting — the server fills the new canvas with a style-matched
extension. If nothing new needs generating and no `prompt` is given, it returns
the flattened image and refunds the unused outputs. **Async** — poll
`get_job_status(kind="image_resize", id=…)`. Cost: `5 × num_outputs ×
resolution`. Scope `images.transform`.
* `target_resolution` *(required)* — `2K`, `4K`, `6K`, `8K`, `10K`, `12K`,
`14K`, `16K`, `20K`, `24K`, `28K`, `32K`, `38K` (classes above `16K` are
**`crystal`-only**).
* **One** of `image_id` or `image_url` *(required)*.
* `model` *(optional)* — `seedvr`, `crystal` (default), `magnific-creative`,
`magnific-precision`.
* `options` *(optional)* — per-model, strictly validated:
`options.magnific_creative` (`prompt`, `optimized_for`,
`creativity`/`hdr`/`resemblance`/`fractality` in −10..10, `engine`) or
`options.magnific_precision` (`sharpen`/`smart_grain`/`ultra_detail` in
0..100, `flavor`). `seedvr`/`crystal` take no options.
Per-model factor/area caps are validated **before** charging. **Async** — poll
`get_job_status(kind="image_upscale", id=…)`. Cost: the resolution tier ×
the model multiplier (Magnific ×3); `crystal` above `12K` prices by output
megapixels — see [pricing](/guides/pricing#upscale). Scope `images.transform`.
* **One** of `image_id` or `image_url` *(required)*.
Produces a transparent PNG; there is no model parameter. **Async** — poll
`get_job_status(kind="image_background_removal", id=…)`. Cost: **1** credit on
a miss; **0** on a cache hit — an owned `image_id` that already has a
background-removed result returns `{ status: "completed", estimated_credits: 0 }`
right away. Scope `images.transform`.
* `svg_acceptance` *(required)* — must be the literal boolean `true`.
* **One** of `image_id` or `image_url` *(required)*.
Produces an **SVG**. An SVG cannot carry a C2PA manifest or watermark, so vector
output is delivered unsigned — see [Content provenance](/guides/content-provenance).
The acknowledgment is disclosure / audit evidence, not a compliance waiver;
without it the call is rejected `422 svg_acceptance_required` and nothing is
charged. Delivery also requires your organization to have accepted the current
ToS/AUP (verified server-side); if not, the call is rejected
`403 svg_phase1_scope_out_required` with no charge. **Async** — poll
`get_job_status(kind="image_vectorize", id=…)`. Cost: **5** on a miss; **0** on
a cache hit. Scope `images.transform`.
* `mode` *(required)* — one of:
* `image_to_video` — animate a start frame. Requires exactly one of
`image_id` / `image_url`; optional `end_image_url` on end-frame-capable
engines; `prompt` optional.
* `text_to_video` — requires `prompt`.
* `text_to_video_styled` — requires `prompt` **and** `style_id`;
`object_ids` / `person_ids` / `setting_ids` optional, plus an optional
`color_palette` (name or id).
* `engine` *(default `veo_3_1_lite`)*, `duration` *(default: the engine's
shortest supported duration, in seconds)*, `aspect_ratio` *(default
`"16:9"`; also `9:16`, `1:1`)*.
**Async** — returns `{ id, status: "pending", estimated_credits }`. Cost:
`5 / second × engine × resolution × audio` multipliers; styled mode adds a
flat 10 for the intermediate image. See [pricing](/guides/pricing#video-generation).
* `kind` *(required)* — one of `image_generation`, `image_edit`, `img2img`,
`image_variation`, `image_resize`, `image_upscale`,
`image_background_removal`, `image_vectorize`, `video`, or `model`. Use the
kind the submitting tool told you to poll.
* `id` *(required)* — the job or model id a submit tool returned.
Returns the status (`pending` → `processing` → `completed` / `failed`) and,
once `completed`, presigned result URLs valid for 24 hours. Requires the scope
of the submitting tool (`models.read` for `kind: "model"`).
For a completed **raster image** job — every image kind **except**
`image_vectorize` — the response also includes a **downscaled inline preview**,
so MCP clients can render it directly, alongside the link to the
full-resolution asset. `image_vectorize` returns an SVG (no raster preview): use
the presigned URL. The preview is a reduced-resolution copy for quick display;
fetch the link for the original.
No parameters. Returns what **this connection** can spend (`available` and
`spendable_plan_credits`) alongside the organization-wide `plan_credits` and
`topup_credits`, the current billing period, and `scope` — the budget regime the
connection draws from. `available` can be lower than `plan_credits` when the
organization reserves plan credits for teams, which is why a job can be refused for
insufficient credits while the organization still shows a balance. See
[`GET /credits`](/api-reference/account/credits) for the full field semantics.
# Security & troubleshooting
Source: https://docs.samsa.ai/mcp-server/troubleshooting
Security, credits and access — plus what to do when a sign-in loops, a tool is missing, or a call is rejected.
## Security, credits & access
MCP tool calls are **real actions on your organization** — they spend credits
and create assets exactly like the app and REST API. Treat an API key connected
to an MCP client like any other production secret.
* **Credits.** The nine media tools — `generate_image`, `edit_image`, `img2img`,
`create_variations`, `resize_image`, `upscale_image`, `remove_background`,
`vectorize_image`, and `generate_video` — draw from your organization's shared
[credit pool](/guides/pricing) at the app's rates. Reads and model management are
free. Check the balance any time with `get_credit_balance`.
* **Scopes.** Every tool requires a [scope](/guides/authentication#scopes). An API
key or OAuth token only exposes the tools its scopes allow — narrow a key to
exactly what an integration needs.
* **Revoking access.** An admin revokes an API key in **Settings → API Keys**;
revocation is terminal and takes effect on the very next call. For an OAuth
connection, disconnect the connector in your client (Claude or ChatGPT connector
settings); access tokens can also be revoked at Samsa's OAuth revocation
endpoint. A revoked credential stops working immediately.
## Troubleshooting
A `401` is the server asking the client to (re)authenticate.
* **OAuth clients:** disconnect the Samsa connector and reconnect to restart
the sign-in. If consent never completes, tell us at
[support@samsa.ai](mailto:support@samsa.ai).
* **API-key clients:** confirm the header is exactly `Authorization: Bearer
samsa_sk_...` and the key is valid — a missing, expired, or revoked key
returns [`401 invalid_api_key`](/guides/errors#invalid_api_key). Create a
fresh key in **Settings → API Keys** if in doubt.
A credential always acts for **one organization**. An **API key** acts for the
organization that owns it — to act for a different org, use a key created in
that org. An **OAuth** connection acts for the account and organization you
signed in with — reconnect to switch. Credits are drawn from, and assets
appear in, that organization.
If the balance this connection can spend can't cover a generation, the submit tool
returns a structured error **before** any job runs — nothing is charged. Which code you
get depends on the connection's budget scope:
[`insufficient_credits`](/guides/errors#insufficient_credits) when the organization has
no active teams,
[`insufficient_team_credits`](/guides/errors#insufficient_team_credits) when it is bound
to a budgeted team, or
[`insufficient_unallocated_credits`](/guides/errors#insufficient_unallocated_credits)
when it is not. An operational failure inside the deduction falls back to the generic
`insufficient_credits` whatever the regime — retry before buying credits — and a
budget-exempt system organization stays under the `org` regime even with active
teams. Check `get_credit_balance` — its `available` is the number a job's cost
must fit into, and its `scope` names the budget regime; under `team` or `unallocated`,
compare `plan_credits` with `scope.remaining` to see which one is binding (under `org`,
`scope.remaining` is `null` and only `plan_credits` can bind) — then top up, raise the
team's budget, or
upgrade in the [Samsa app](https://app.samsa.ai). Reads are always free.
Three independent limits can slow a burst of calls:
* **Per-key request rate** — 60 requests/minute; excess returns
[`rate_limited`](/guides/errors#rate_limited).
* **Per-organization concurrency** — at most 5 in-flight jobs at once; a sixth
returns [`too_many_active_jobs`](/guides/errors#too_many_active_jobs). Let
jobs finish (poll `get_job_status`) before submitting more.
* **MCP transport concurrency** — a burst of simultaneous `/mcp` requests on a
single worker can return a transient `429 concurrency_limit_exceeded` with
`Retry-After: 1`. Wait one second and retry.
See [Rate limits](/guides/rate-limits) for the full picture and back-off
guidance.
Tools you can't call are hidden or rejected because the connected credential
lacks their [scope](/guides/authentication#scopes). For example,
`generate_image` needs `images.generate`. Edit the key's scopes (or issue a new
key) in **Settings → API Keys**, then reconnect.
# Quickstart
Source: https://docs.samsa.ai/quickstart
Create an API key and generate your first image with a trained style model in five steps.
This guide takes you from zero to a finished image in five steps: create a key,
verify it, submit a generation, poll for the result, and download it. Every call
targets the base URL:
```
https://api.samsa.ai/public/v1
```
The examples use a fake key (`samsa_sk_example…`) and placeholder ids. Replace
them with your own. Store your key in an environment variable so it never lands
in source control:
```bash theme={null}
export SAMSA_API_KEY="samsa_sk_exampleXf9Lp2QyaBcDeFgHiJkLmNoPqRsTuVwx"
```
## Create an API key
API keys are **organization-owned** and can only be created by an organization
**admin** (`OWNER` or `ADMIN`).
In the [Samsa app](https://app.samsa.ai), go to your organization settings and
open the **API Keys** tab.
Give the key a name. By default it is granted **all scopes**
(`images.generate`, `images.edit`, `videos.generate`, `models.read`,
`models.write`, `usage.read`); narrow them if the integration needs less.
You can also set an optional expiry.
The full key (`samsa_sk_…`) is shown **exactly once**, at creation. Samsa
stores only a hash and can never display it again. Copy it immediately and
keep it somewhere safe — if you lose it, revoke the key and create a new one.
Keys act **for their organization**: credits are drawn from the organization's
pool, and any images or videos you generate appear in the app under the account
of the admin who created the key.
## Verify the key with `GET /me`
`GET /me` is the fastest way to confirm a key works. It returns the key's
organization, its safe metadata (prefix, scopes, expiry — never the secret), and
the organization's available credit balance.
```bash curl theme={null}
curl https://api.samsa.ai/public/v1/me \
-H "Authorization: Bearer $SAMSA_API_KEY"
```
```python Python theme={null}
import os
import requests
BASE_URL = "https://api.samsa.ai/public/v1"
API_KEY = os.environ["SAMSA_API_KEY"]
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
resp = requests.get(f"{BASE_URL}/me", headers=HEADERS)
resp.raise_for_status()
print(resp.json())
```
```typescript TypeScript theme={null}
const BASE_URL = "https://api.samsa.ai/public/v1";
const API_KEY = process.env.SAMSA_API_KEY!;
const headers = { Authorization: `Bearer ${API_KEY}` };
const resp = await fetch(`${BASE_URL}/me`, { headers });
if (!resp.ok) throw new Error(`GET /me failed: ${resp.status}`);
console.log(await resp.json());
```
```json Response theme={null}
{
"organization": {
"id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"name": "Acme Inc"
},
"api_key": {
"id": "9c8d7e6f-5a4b-4c3d-2e1f-0a9b8c7d6e5f",
"name": "Production key",
"prefix": "samsa_sk_exam",
"scopes": [
"images.edit",
"images.generate",
"models.read",
"models.write",
"usage.read",
"videos.generate"
],
"expires_at": null
},
"credits": {
"available": 1450
}
}
```
## Generate an image
Submit a prompt to `POST /images/generations`. Here we also compose one of the
organization's trained **style** models by passing its id or name as `style_id` —
a name resolves to a model visible to you. You can combine `object_ids`,
`person_ids`, `setting_ids`, and a `color_palette_id` the same way — each by name
or id. The request returns `202` immediately with a job `id`; the image is
produced asynchronously.
```bash curl theme={null}
curl -X POST https://api.samsa.ai/public/v1/images/generations \
-H "Authorization: Bearer $SAMSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "A minimalist product shot of a ceramic mug on linen, soft daylight",
"style_id": "2b9d1f7a-3c4e-4a5b-9c8d-0e1f2a3b4c5d",
"aspect_ratio": "1:1",
"resolution": "1K",
"num_outputs": 1
}'
```
```python Python theme={null}
payload = {
"prompt": "A minimalist product shot of a ceramic mug on linen, soft daylight",
"style_id": "2b9d1f7a-3c4e-4a5b-9c8d-0e1f2a3b4c5d",
"aspect_ratio": "1:1",
"resolution": "1K",
"num_outputs": 1,
}
resp = requests.post(
f"{BASE_URL}/images/generations",
headers={**HEADERS, "Content-Type": "application/json"},
json=payload,
)
resp.raise_for_status()
job = resp.json()
generation_id = job["id"]
print(job)
```
```typescript TypeScript theme={null}
const payload = {
prompt: "A minimalist product shot of a ceramic mug on linen, soft daylight",
style_id: "2b9d1f7a-3c4e-4a5b-9c8d-0e1f2a3b4c5d",
aspect_ratio: "1:1",
resolution: "1K",
num_outputs: 1,
};
const submit = await fetch(`${BASE_URL}/images/generations`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!submit.ok) throw new Error(`generation failed: ${submit.status}`);
const job = await submit.json();
const generationId = job.id;
console.log(job);
```
```json Response — 202 Accepted theme={null}
{
"id": "7f9c0e2a-1b3d-4c5e-8f6a-9b0c1d2e3f40",
"status": "pending",
"estimated_credits": 5
}
```
The default engine is `nano_banana_pro` (pass `engine` to choose
`nano_banana_2`). `num_outputs` defaults to **1**; each output costs
`5` credits at `1K`, scaling with resolution (`1K` ×1, `2K` ×2, `4K` ×4). The
`style_id` — an id or a name — must reference a `completed` model visible to you.
## Poll for the result
Poll `GET /images/generations/{id}` until `status` is `completed` (or `failed`).
Statuses are `pending`, `processing`, `completed`, `failed`, and `cancelled`.
```bash curl theme={null}
curl https://api.samsa.ai/public/v1/images/generations/7f9c0e2a-1b3d-4c5e-8f6a-9b0c1d2e3f40 \
-H "Authorization: Bearer $SAMSA_API_KEY"
```
```python Python theme={null}
import time
while True:
resp = requests.get(
f"{BASE_URL}/images/generations/{generation_id}",
headers=HEADERS,
)
resp.raise_for_status()
result = resp.json()
if result["status"] in ("completed", "failed", "cancelled"):
break
time.sleep(2)
print(result)
```
```typescript TypeScript theme={null}
async function poll(id: string) {
while (true) {
const resp = await fetch(`${BASE_URL}/images/generations/${id}`, { headers });
if (!resp.ok) throw new Error(`poll failed: ${resp.status}`);
const result = await resp.json();
if (["completed", "failed", "cancelled"].includes(result.status)) {
return result;
}
await new Promise((r) => setTimeout(r, 2000));
}
}
const result = await poll(generationId);
console.log(result);
```
```json Response — completed theme={null}
{
"id": "7f9c0e2a-1b3d-4c5e-8f6a-9b0c1d2e3f40",
"status": "completed",
"created_at": "2026-07-02T12:00:00Z",
"credits_used": 5,
"images": [
{
"id": "d4c3b2a1-6f5e-4b3a-9d8c-1e0f2a3b4c5d",
"url": "https://cdn.samsa.ai/user-.../7f9c0e2a.png?X-Amz-Signature=...",
"thumbnail_url": "https://cdn.samsa.ai/user-.../7f9c0e2a-thumb.png?X-Amz-Signature=...",
"width": 1024,
"height": 1024,
"seed": 128390
}
],
"error": null
}
```
## Download the result
Each entry in `images` carries a presigned HTTPS `url` that is valid for **24
hours** — download and store the asset before it expires.
```bash curl theme={null}
curl -o mug.png \
"https://cdn.samsa.ai/user-.../7f9c0e2a.png?X-Amz-Signature=..."
```
```python Python theme={null}
image_url = result["images"][0]["url"]
img = requests.get(image_url)
img.raise_for_status()
with open("mug.png", "wb") as f:
f.write(img.content)
```
```typescript TypeScript theme={null}
const imageUrl = result.images[0].url;
const img = await fetch(imageUrl);
const buffer = Buffer.from(await img.arrayBuffer());
await import("node:fs/promises").then((fs) => fs.writeFile("mug.png", buffer));
```
## Next steps
The base URL, authentication, and the conventions every endpoint shares.
Connect Samsa to Claude, ChatGPT, or any MCP client and generate media as
tools — over OAuth or an API key.
Prefer push over polling? Pass a `webhook_url` on any generation request to
receive a signed callback the moment the job reaches a terminal status.