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