Partner API

Developer docs for white-label partners that bring their own users: the sign-in hand-off, per-user credits, usage for your monthly invoice, and user management.

The Partner API connects your own product to your Sozee white-label site. It is for companies that already have their own login and users: you sign those users in to Sozee from your server, give them credits, read what they used, and manage their accounts.

View as Markdown · OpenAPI document · Questions: support@sozee.ai

Your users never buy anything from Sozee. You decide how many credits each user has, and Sozee invoices you each month for the credits your users spent.

On this page

How it works

  1. Your user clicks. A button such as "Open studio" on your site.
  2. Your server calls. POST /sessions with your id for that user.
  3. You redirect. The browser goes to the launch_url in the answer.
  4. Signed in. The user lands on your Sozee site. No second login, no Sozee password.
  5. Credits and usage. Your server adds credits and reads balances and usage whenever it needs to.

Before you start

Sozee gives you:

You give Sozee:

Your API key is a secret. Use it only on your server. Never put a key in a browser, a mobile app, a public repository or a log.

Optional settings

Sozee can switch these on for your site. Both are off unless you ask.

Quickstart

Replace szp_live_… with your key. First, check it:

curl https://app.sozee.ai/api/partner/v1/me \
  -H "Authorization: Bearer szp_live_…"

Answer

{
  "partner_id": "yourbrand",
  "name": "Your Brand",
  "environment": "live",
  "site_url": "https://yourbrand.sozee.ai"
}

Create a sign-in link for one of your users:

curl -X POST https://app.sozee.ai/api/partner/v1/sessions \
  -H "Authorization: Bearer szp_live_…" \
  -H "Content-Type: application/json" \
  -d '{"user_id": "user_8f2c1", "email": "sam@example.com", "name": "Sam Lee"}'

Answer

{
  "launch_url": "https://yourbrand.sozee.ai/launch?code=one-time-code",
  "expires_in": 120,
  "user": {
    "user_id": "user_8f2c1",
    "status": "active",
    "created_at": "2026-11-12T21:31:30.846Z",
    "credits": {
      "balance": 0.0
    }
  }
}

Open the launch_url in a browser within 120 seconds and you are signed in as that user. A new user has 0 credits, so give them some:

curl -X POST https://app.sozee.ai/api/partner/v1/credits \
  -H "Authorization: Bearer szp_live_…" \
  -H "Content-Type: application/json" \
  -d '{"user_id": "user_8f2c1", "amount": 50, "reference": "inv_2026_11_0042"}'

Answer

{
  "user_id": "user_8f2c1",
  "balance": 50.0,
  "amount": 50.0,
  "reference": "inv_2026_11_0042",
  "replayed": false
}

Credit amounts come back as JSON numbers with a decimal point, for example 50.0.

Base URL and authentication

WhatValue
Base URLhttps://app.sozee.ai/api/partner/v1
Key prefixszp_live_
Body formatJSON, with Content-Type: application/json

Every call except GET /openapi.json needs this header:

Header

Authorization: Bearer szp_live_…

A missing or wrong key is refused with 401 invalid_api_key.

Testing

To try your integration without touching real users, use a made-up user_id such as test_user_1 and delete it when you are done. If you want a separate test site with its own address and key, ask us.

Key rotation

Sozee creates your keys. Two can be live at once, so you can rotate without downtime: ask us for a new key, deploy it, check your calls succeed with it, then ask us to revoke the old one.

Sign your users in

POST /sessions creates a one-time sign-in link for one user.

Body

{"user_id": "user_8f2c1", "email": "sam@example.com", "name": "Sam Lee"}

The first call for a new user_id creates the user. There is no import step and no separate create call. The answer carries the user in the same shape as GET /user:

Answer

{
  "launch_url": "https://yourbrand.sozee.ai/launch?code=one-time-code",
  "expires_in": 120,
  "user": {
    "user_id": "user_8f2c1",
    "status": "active",
    "created_at": "2026-11-12T21:31:30.846Z",
    "credits": {
      "balance": 0.0
    }
  }
}

Create the link on click. The link works once and expires 120 seconds after it is created. Create it at the moment the user clicks, on every click, and redirect at once. Never create links ahead of time, store them, email them or put them in a page.

If the user is disabled or deleted, the call answers 409 user_disabled or 409 user_deleted and no link is created. Show your own message in that case.

Example route

This route checks your own login, asks Sozee for a link and redirects. The Node version uses the fetch built into Node 18 and later.

Node (Express)

import express from "express";
import session from "express-session";

const SOZEE_API = process.env.SOZEE_API_BASE; // https://app.sozee.ai/api/partner/v1
const SOZEE_KEY = process.env.SOZEE_API_KEY;  // szp_live_…, server-side only
const UNAVAILABLE = "The studio is not available right now. Try again in a minute.";

const app = express();
app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false }));

app.get("/open-sozee", async (req, res) => {
  const user = req.session.user; // set by your own login
  if (!user) return res.redirect("/login?next=/open-sozee");

  try {
    const r = await fetch(`${SOZEE_API}/sessions`, {
      method: "POST",
      headers: { Authorization: `Bearer ${SOZEE_KEY}`, "Content-Type": "application/json" },
      body: JSON.stringify({ user_id: String(user.id), email: user.email, name: user.name }),
      signal: AbortSignal.timeout(10000),
    });
    const body = await r.json();

    if (!r.ok) {
      const code = body.error && body.error.code;
      if (code === "user_disabled" || code === "user_deleted") {
        return res.status(403).send("Your studio access is turned off. Contact support.");
      }
      return res.status(502).send(UNAVAILABLE);
    }

    res.set("Cache-Control", "no-store");
    res.redirect(302, body.launch_url);
  } catch (err) {
    // A timeout, a network error or an answer that is not JSON.
    res.status(502).send(UNAVAILABLE);
  }
});

app.listen(3000);

Python (Flask)

import os
import requests
from flask import Flask, redirect, session, make_response

SOZEE_API = os.environ["SOZEE_API_BASE"]  # https://app.sozee.ai/api/partner/v1
SOZEE_KEY = os.environ["SOZEE_API_KEY"]   # szp_live_…, server-side only
UNAVAILABLE = "The studio is not available right now. Try again in a minute."

app = Flask(__name__)
app.secret_key = os.environ["SESSION_SECRET"]

@app.get("/open-sozee")
def open_sozee():
    user = session.get("user")  # set by your own login
    if not user:
        return redirect("/login?next=/open-sozee")

    try:
        r = requests.post(
            f"{SOZEE_API}/sessions",
            headers={"Authorization": f"Bearer {SOZEE_KEY}"},
            json={"user_id": str(user["id"]), "email": user.get("email"), "name": user.get("name")},
            timeout=10,
        )
        body = r.json()
    except (requests.RequestException, ValueError):
        # A timeout, a network error or an answer that is not JSON.
        return UNAVAILABLE, 502

    if not r.ok:
        code = body.get("error", {}).get("code")
        if code in ("user_disabled", "user_deleted"):
            return "Your studio access is turned off. Contact support.", 403
        return UNAVAILABLE, 502

    resp = make_response(redirect(body["launch_url"], code=302))
    resp.headers["Cache-Control"] = "no-store"
    return resp

What your users see

Signed-out visitors

Anyone who arrives at your Sozee site signed out, for example from a bookmark, is sent to your signInUrl once. If they come straight back still signed out, they see "Open Your Brand from your Your Brand account." with a link. So signInUrl must either start a session or show your own login: point it at the same /open-sozee route. After signing out, a user sees "You're signed out." with a link back to you.

Note. Opening your Sozee site signs that browser out of any other Sozee account it was signed in to.

Credits

Every user starts with 0 credits. Two calls change a balance, and both create the user if Sozee has never seen them, so you can give credits before the first sign-in:

Numbers must be JSON numbers: a string such as "50" is refused with 422 validation_error.

curl -X POST https://app.sozee.ai/api/partner/v1/credits/set \
  -H "Authorization: Bearer szp_live_…" \
  -H "Content-Type: application/json" \
  -d '{"user_id": "user_8f2c1", "balance": 20, "reference": "allowance_2026_12_user_8f2c1"}'

Answer

{
  "user_id": "user_8f2c1",
  "balance": 20.0,
  "previous_balance": 50.0,
  "reference": "allowance_2026_12_user_8f2c1",
  "replayed": false
}

The balance in a credits answer is the balance right after your change.

The reference makes retries safe

Every credit call needs a reference of 1 to 128 characters. Use your own transaction or invoice id. One reference identifies one credit operation for your whole account, not per user:

Retry with the same reference. If a credit call times out, send exactly the same call again. You will never add credits twice.

POST /credits/set answers 409 balance_changed if the user's balance kept changing while Sozee tried to set it, for example while they were generating. Wait a moment and send it again with the same reference.

Reading a balance

curl -G https://app.sozee.ai/api/partner/v1/user \
  -H "Authorization: Bearer szp_live_…" \
  --data-urlencode "user_id=user_8f2c1"

Answer

{
  "user_id": "user_8f2c1",
  "status": "active",
  "created_at": "2026-11-12T21:31:30.846Z",
  "credits": {
    "balance": 20.0
  }
}

A deleted user answers {"user_id": "user_8f2c1", "status": "deleted"}, with no created_at and no credits. A user_id Sozee has never seen answers 404 user_not_found. Send user_id URL-encoded, as curl --data-urlencode does.

When a user runs out

Credits are spent as the user creates. When a user runs out, they see "You're out of credits. Get more from Your Brand." with a "Get credits" button that opens your creditsUrl in a new tab. Their balance refreshes when they come back. Sozee never sells credits or plans to your users and never shows them Sozee prices.

Usage and your invoice

GET /usage reports credits per user for a date range.

curl -G https://app.sozee.ai/api/partner/v1/usage \
  -H "Authorization: Bearer szp_live_…" \
  -d from=2026-11-01 -d to=2026-12-01

Answer

{
  "from": "2026-11-01",
  "to": "2026-12-01",
  "complete": true,
  "next_cursor": null,
  "users": [
    {
      "user_id": "user_8f2c1",
      "credits_added": 20.0,
      "credits_spent": 3.0,
      "credits_refunded": 1.0,
      "credits_used": 2.0
    }
  ],
  "totals": {
    "credits_added": 20.0,
    "credits_spent": 3.0,
    "credits_refunded": 1.0,
    "credits_used": 2.0
  }
}

Each user has four numbers:

FieldMeaning
credits_addedThe net of your adds and balance changes in the range. It can be negative, for example when a set lowered the balance.
credits_spentCredits the user spent on generations.
credits_refundedCredits returned to the user for failed or refunded generations. Netted automatically.
credits_usedcredits_spent minus credits_refunded. This is the billable number.

How your invoice is worked out. You are invoiced monthly for credits_used across your users at the rate in your agreement. Credits you add but users do not spend are not billed.

When complete is false, the report was too large for one answer. Call again with the same from, to and user_id, plus cursor set to the next_cursor you received, and add the numbers from each page together. next_cursor is null when complete is true.

Manage users

Each of these calls takes {"user_id": "…"} in the body. A user_id Sozee has never seen answers 404 user_not_found.

CallWhat it does
POST /users/disableTakes effect at once: their open studio stops working and they are sent to your signInUrl. POST /sessions then answers 409 user_disabled, so show your own message there. Reversible.
POST /users/enableLets a disabled user back in. Their credits are where they were.
POST /users/signoutEnds the user's Sozee sessions, within about a minute. Call it from your own logout. The user can come back through your site at any time.
POST /users/deleteCloses the account for good. That user_id can never be used again, and GET /user shows "status": "deleted". Billing records are kept. Calling it again answers the same.

Disable, enable and sign out on a deleted user answer 409 user_deleted.

curl -X POST https://app.sozee.ai/api/partner/v1/users/disable \
  -H "Authorization: Bearer szp_live_…" \
  -H "Content-Type: application/json" \
  -d '{"user_id": "user_8f2c1"}'

Answer

{"user_id": "user_8f2c1", "status": "disabled"}

Deleting is not erasing. Deleting a user closes their account. It does not erase the content they stored. To have a user's stored content erased, for example for a privacy request, email support@sozee.ai with the user_id.

If Sozee could not finish ending a user's sessions, disable answers 503 revocation_incomplete, and if a delete could not finish, 503 dependency_unavailable. In both cases the user is already blocked. Send the same call again until it succeeds.

Errors

Every error has the same shape. Read code, not message: the message is for people and may change.

Error

{"error": {"code": "user_disabled", "message": "This user is disabled. Enable it first."}}
StatusCodeWhat to do
400invalid_requestThe body is not valid JSON. Fix the request.
401invalid_api_keyThe API key is missing or not valid. Check the key.
403partner_disabledYour partner account is turned off. Contact Sozee.
404user_not_foundSozee has never seen this user_id. Check the id, or create the user with a session or credit call.
404not_foundNo such endpoint. Check the path.
405method_not_allowedWrong HTTP method for this path. Check the reference.
409user_disabledThe user is disabled. Enable them first if that is what you want.
409user_deletedThe user was deleted. The user_id cannot be used again.
409reference_conflictThis reference was already used for a different request. Use a new reference.
409balance_changedThe balance kept changing during a set. Send the same call again shortly.
409host_not_supportedYour site is not on a Sozee address. Contact Sozee.
422validation_errorA field is missing or out of range. fields names which. Fix the request.
429rate_limitedToo many calls. Wait for the number of seconds in the Retry-After header, then retry.
502upstream_unavailableSozee could not reach a service it needs. Retry with backoff.
503partner_api_disabledNew sessions and credit changes are paused. Reads and user management still work. Retry later.
503dependency_unavailableA service Sozee depends on did not answer. Retry with backoff.
503revocation_incompleteThe user is blocked but some sessions were not ended yet. Retry the same call.
503usage_unavailableUsage reports are not available right now. Retry later.

A validation error adds fields, which maps each field name to a message:

Error

{
  "error": {
    "code": "validation_error",
    "message": "Some fields are not valid.",
    "fields": {
      "amount": "must be greater than 0"
    }
  }
}

Retry rules

Limits

LimitValue
Session calls300 a minute per key
Credit calls600 a minute per key
Usage calls120 a minute per key
Other callsNot limited today
user_id1 to 128 characters, no control characters
reference1 to 128 characters
amountAbove 0, at most 2 decimals, at most 1,000,000 per call
balance (set)0 to 1,000,000, at most 2 decimals
Usage rangeUp to 92 days

Not available yet

Go-live checklist

Brief for your AI coding agent

Paste this into your AI coding tool. It is complete on its own.

Goal: connect our product to our Sozee white-label site using the Sozee Partner API.
Our users sign in to our product as they do today. From our product they open the
Sozee studio already signed in. We give them credits and read their usage.

API
- Base URL: https://app.sozee.ai/api/partner/v1 (there is one base URL; keys start szp_live_)
- Header on every call: Authorization: Bearer <key>. JSON bodies with Content-Type: application/json.
- Read the key from an environment variable (SOZEE_API_KEY) and the base URL from SOZEE_API_BASE.
- OpenAPI document (no key needed): https://app.sozee.ai/api/partner/v1/openapi.json
- To test, use made-up user ids such as test_user_1 and delete them afterwards.

Calls
- GET /me -> {partner_id, name, environment: "live", site_url}
- POST /sessions {user_id, email?, name?} -> {launch_url, expires_in: 120, user}
  Creates the user on first use. user has the GET /user shape. Redirect the browser to
  launch_url with a 302.
- GET /user?user_id=... -> {user_id, status ("active" or "disabled"), created_at, credits: {balance}}
  A deleted user -> {user_id, status: "deleted"}. Never seen -> 404 user_not_found.
- POST /credits {user_id, amount, reference} -> {user_id, balance, amount, reference, replayed}
  Adds credits. Creates the user if never seen. amount is a JSON number above 0, at most
  2 decimals, at most 1,000,000 (strings such as "50" are refused).
- POST /credits/set {user_id, balance, reference} -> {user_id, balance, previous_balance, reference, replayed}
  Sets the balance: 0 to 1,000,000, at most 2 decimals.
- GET /usage?from=YYYY-MM-DD&to=YYYY-MM-DD[&user_id=...][&cursor=...]
  -> {from, to, complete, next_cursor, users: [{user_id, credits_added, credits_spent,
     credits_refunded, credits_used}], totals: {same four}}
  UTC dates, from included, to excluded, at most 92 days. credits_used is what we are billed for.
  While complete is false, call again with cursor=next_cursor (same from, to, user_id) and add
  the numbers up.
- POST /users/disable {user_id} -> {user_id, status: "disabled"}
- POST /users/enable {user_id} -> {user_id, status: "active"}
- POST /users/signout {user_id} -> {user_id, status}
- POST /users/delete {user_id} -> {user_id, status: "deleted"} (permanent)
- Unknown user_id: GET /user and the four /users calls answer 404 user_not_found.
- Errors: {"error": {"code": "...", "message": "..."}}. Branch on code, never on message.
  422 validation_error adds "fields": {field name: message}.

Rules that are easy to get wrong
1. The API key is a server-side secret. Never send it to a browser or app, never log it.
2. user_id is our own stable id for the user, sent in the JSON body (or the query string on GET),
   never in the URL path. Send it exactly as we store it. Do not use the email address.
3. Create the launch link when the user clicks, on every click, then redirect at once.
   It works once and expires after 120 seconds. Never cache or pre-create it. Send
   Cache-Control: no-store on the redirect.
4. Every credit call sends a reference from our own records (transaction or invoice id).
   One reference is one credit operation for our whole account. Repeating the identical call
   is always safe and answers replayed: true. Reusing a reference with a different user,
   amount, balance or credit call is 409 reference_conflict.
5. Handle 409 user_disabled and 409 user_deleted from /sessions and the credit calls with a
   clear message to the user. A deleted user_id can never be used again.
6. Retries: on 429 wait for the Retry-After header. On 5xx or a network error retry with
   backoff (1, 2, 4, 8 seconds). Never retry another 4xx unchanged, except 409 balance_changed.
7. New users have 0 credits: add credits before or right after their first session.

Build
- A GET route /open-sozee: if the visitor is not signed in to our product, send them to our
  login and back. Otherwise call POST /sessions with their id, email and name and 302 to
  launch_url. This route's full URL is our signInUrl.
- Our logout calls POST /users/signout. Closing an account calls POST /users/delete.
- Wherever we grant credits (purchase, plan renewal, allowance), call POST /credits or
  POST /credits/set with a reference.
- A monthly job that calls GET /usage for last month, follows next_cursor, and stores the totals.

Definition of done
- GET /me answers environment "live" and our site address.
- Clicking our "Open studio" button lands a signed-in user on our Sozee site, and a second
  click creates a new link.
- With a made-up user_id such as test_user_1: a credit call sent twice with the same reference
  changes the balance once and the second answer has replayed: true; after
  POST /users/disable our own message shows instead of the studio; POST /users/delete removes it.
- The key appears in no client bundle, repository file or log line.
- Usage for a date range returns, and pages are followed until complete is true.

Reference

All paths are relative to https://app.sozee.ai/api/partner/v1. The machine-readable description of every call is the OpenAPI document.

GET /me

Check your key and see which partner it belongs to.

Answer

FieldTypeNotes
partner_idstringYour partner id.
namestringYour partner name.
environmentstringAlways live.
site_urlstringYour Sozee site address.

Errors: 401 invalid_api_key.

POST /sessions

Create a one-time sign-in link for a user. Creates the user on first use.

Body

FieldTypeNotes
user_idstringYour id for the user, 1 to 128 characters, used exactly as sent.
emailstring, optionalKept for your and our reference. Never shown to the user, used to sign in or emailed.
namestring, optionalShown to the user in the studio's account menu.

Answer

FieldTypeNotes
launch_urlstringRedirect the browser here. Works once.
expires_inintegerSeconds the link stays valid: 120.
userobjectThe user, in the same shape as GET /user.

Errors: 409 user_disabled, 409 user_deleted, 409 host_not_supported, 422 validation_error, 429 rate_limited, 503 partner_api_disabled.

GET /user

Read a user's status and credit balance.

Query

FieldTypeNotes
user_idquery stringYour id for the user, URL-encoded.

Answer

FieldTypeNotes
user_idstringYour id for the user.
statusstringactive, disabled or deleted.
created_atstringISO 8601 UTC with milliseconds. Absent for a deleted user.
credits.balancenumberThe current balance. Absent for a deleted user.

Errors: 404 user_not_found.

POST /credits

Add credits to a user. Creates the user if Sozee has never seen them.

Body

FieldTypeNotes
user_idstringYour id for the user, 1 to 128 characters, used exactly as sent.
amountnumberAbove 0, at most 2 decimals, at most 1,000,000. A string such as "50" is refused.
referencestringYour own id for this credit operation, 1 to 128 characters. Unique across your account.

Answer

FieldTypeNotes
user_idstringYour id for the user.
balancenumberThe balance right after the change. On a replay, the balance now (absent for a deleted user).
amountnumberThe amount added.
referencestringYour reference.
replayedbooleantrue when this reference was already applied and nothing changed.

Errors: 409 user_disabled, 409 user_deleted, 409 reference_conflict, 422 validation_error, 429 rate_limited, 503 partner_api_disabled.

POST /credits/set

Set a user's balance to an exact number, for allowances and expiry. Creates the user if Sozee has never seen them.

Body

FieldTypeNotes
user_idstringYour id for the user, 1 to 128 characters, used exactly as sent.
balancenumber0 to 1,000,000, at most 2 decimals.
referencestringYour own id for this credit operation, 1 to 128 characters. Unique across your account.

Answer

FieldTypeNotes
user_idstringYour id for the user.
balancenumberThe balance right after the change. On a replay, the balance now (absent for a deleted user).
previous_balancenumberThe balance before the change. On a replay, the value from the first time.
referencestringYour reference.
replayedbooleantrue when this reference was already applied and nothing changed.

Errors: 409 user_disabled, 409 user_deleted, 409 reference_conflict, 409 balance_changed, 422 validation_error, 429 rate_limited, 503 partner_api_disabled.

GET /usage

Report credits added, spent, refunded and used per user for a date range.

Query

FieldTypeNotes
fromquery, YYYY-MM-DDFirst UTC day, included.
toquery, YYYY-MM-DDLast UTC day, not included. At most 92 days after from.
user_idquery, optionalLimit the report to one user.
cursorquery, optionalThe next_cursor from the previous page.

Answer

FieldTypeNotes
fromstringAs sent.
tostringAs sent.
completebooleanfalse when more pages follow.
next_cursorstring or nullSend it as cursor to get the next page. null when complete.
usersarrayOne entry per user: user_id, credits_added, credits_spent, credits_refunded, credits_used.
totalsobjectThe same four numbers summed over this page.

Errors: 404 user_not_found, 422 validation_error, 429 rate_limited, 503 usage_unavailable.

POST /users/disable

Block a user at once and end their Sozee sessions. Reversible.

Body

FieldTypeNotes
user_idstringYour id for the user, 1 to 128 characters, used exactly as sent.

Answer

FieldTypeNotes
user_idstringYour id for the user.
statusstringThe user's status after the call.

Errors: 404 user_not_found, 409 user_deleted, 503 revocation_incomplete.

POST /users/enable

Let a disabled user back in.

Body

FieldTypeNotes
user_idstringYour id for the user, 1 to 128 characters, used exactly as sent.

Answer

FieldTypeNotes
user_idstringYour id for the user.
statusstringThe user's status after the call.

Errors: 404 user_not_found, 409 user_deleted.

POST /users/signout

End a user's Sozee sessions, within about a minute. Call it from your own logout.

Body

FieldTypeNotes
user_idstringYour id for the user, 1 to 128 characters, used exactly as sent.

Answer

FieldTypeNotes
user_idstringYour id for the user.
statusstringThe user's status after the call.

Errors: 404 user_not_found, 409 user_deleted.

POST /users/delete

Close a user's account for good. The user_id can never be used again. Calling it again answers the same.

Body

FieldTypeNotes
user_idstringYour id for the user, 1 to 128 characters, used exactly as sent.

Answer

FieldTypeNotes
user_idstringYour id for the user.
statusstringThe user's status after the call.

Errors: 404 user_not_found, 503 dependency_unavailable.

GET /openapi.json

The OpenAPI 3 document for these calls. No key needed.

Answer

FieldTypeNotes
(document)JSONThe machine-readable description of every call.

Questions about your integration

Email support@sozee.ai and include your partner id. To set up a white-label site, start with the white-label page.

Email support · White label