---
title: "Sozee Partner API: sign in your users, give them credits, read usage"
description: "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."
url: https://www.sozee.ai/docs/partners
---

# 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](https://www.sozee.ai/docs/partners.md) · [OpenAPI document](https://app.sozee.ai/api/partner/v1/openapi.json) · 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](#how-it-works)
- [Before you start](#before-you-start)
- [Quickstart](#quickstart)
- [Base URL and authentication](#base-url-and-authentication)
- [Sign your users in](#sign-your-users-in)
- [Credits](#credits)
- [Usage and your invoice](#usage-and-your-invoice)
- [Manage users](#manage-users)
- [Errors](#errors)
- [Limits](#limits)
- [Go-live checklist](#go-live-checklist)
- [Brief for your AI coding agent](#brief-for-your-ai-coding-agent)
- [Reference](#reference)

## 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:**

- Your site address, for example `yourbrand.sozee.ai`. This is where your users create.
- An API key that starts with `szp_live_`. Sozee creates it for you and shows it once: store it in your secrets manager straight away.

**You give Sozee:**

- Your logo and brand color, for the look of your site.
- `signInUrl`: the page on your site that starts a Sozee session (the route in [Sign your users in](#sign-your-users-in)). Sozee sends signed-out visitors there.
- `creditsUrl`: the page on your site where a user gets more credits. Sozee links to it when a user runs out.

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

- A "Help" link in the studio menu that opens your own help center. Give us the URL.
- Sozee's support chat in the studio. The chat window carries Sozee's name.

## Quickstart

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

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

**Answer**

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

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

```bash
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**

```json
{
  "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:

```bash
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**

```json
{
  "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

| What | Value |
|---|---|
| Base URL | `https://app.sozee.ai/api/partner/v1` |
| Key prefix | `szp_live_` |
| Body format | JSON, with `Content-Type: application/json` |

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

**Header**

```text
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**

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

- `user_id` is required. It is your id for the user, exactly as you store it. It travels in the request body, never in the URL, so it may contain any characters, including `/`, `+`, `@` and `|`. It is case-sensitive and never trimmed.
- Do not send an email address as `user_id` unless an email address is your user id: a user who changes their email would become a new user.
- `name` is optional and is shown to the user in the studio's account menu.
- `email` is optional and is kept for your and our reference only. It is never shown to the user, never used to sign in and never emailed by Sozee.

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**

```json
{
  "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)**

```js
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)**

```python
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

- A first-time user lands on a short character setup. They have 0 credits until you add some, so add credits before or right after their first session.
- A link that was already used, or is older than 120 seconds, shows "That link has expired. Go back to Your Brand and open it again." with a link to your `signInUrl`. There is no automatic redirect.
- Users cannot change their email or password inside the studio: their identity is yours. The studio has no sign-up or login form on your site.

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

- `POST /credits` adds credits: `user_id`, `amount` and `reference`. `amount` is a JSON number above 0, with at most 2 decimals, at most 1,000,000.
- `POST /credits/set` sets the balance to an exact number: `user_id`, `balance` and `reference`. `balance` is 0 to 1,000,000, with at most 2 decimals. Use it for monthly allowances (set to 300 on the first of the month) and for expiry (set to 0).

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

```bash
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**

```json
{
  "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:

- Repeating the identical call is always safe. It changes nothing and answers `"replayed": true`, even if the user was disabled or deleted in between. On a replay, `balance` is the balance now (absent for a deleted user), and for a set, `previous_balance` is the value from the first time.
- Reusing a reference with a different user, a different amount or balance, or on the other credit call is refused with 409 `reference_conflict`.
- A new reference for a disabled or deleted user is refused with 409 `user_disabled` or 409 `user_deleted`.

> **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

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

**Answer**

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

- `from` and `to` are UTC dates (`YYYY-MM-DD`). `from` is included, `to` is not, so a calendar month is `from=2026-11-01&to=2026-12-01`. A range can be up to 92 days.
- `user_id` is optional and limits the report to one user.

```bash
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**

```json
{
  "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:

| Field | Meaning |
|---|---|
| `credits_added` | The net of your adds and balance changes in the range. It can be negative, for example when a set lowered the balance. |
| `credits_spent` | Credits the user spent on generations. |
| `credits_refunded` | Credits returned to the user for failed or refunded generations. Netted automatically. |
| `credits_used` | `credits_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`.

| Call | What it does |
|---|---|
| `POST /users/disable` | Takes 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/enable` | Lets a disabled user back in. Their credits are where they were. |
| `POST /users/signout` | Ends 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/delete` | Closes 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`.

```bash
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**

```json
{"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**

```json
{"error": {"code": "user_disabled", "message": "This user is disabled. Enable it first."}}
```

| Status | Code | What to do |
|---|---|---|
| 400 | `invalid_request` | The body is not valid JSON. Fix the request. |
| 401 | `invalid_api_key` | The API key is missing or not valid. Check the key. |
| 403 | `partner_disabled` | Your partner account is turned off. Contact Sozee. |
| 404 | `user_not_found` | Sozee has never seen this `user_id`. Check the id, or create the user with a session or credit call. |
| 404 | `not_found` | No such endpoint. Check the path. |
| 405 | `method_not_allowed` | Wrong HTTP method for this path. Check the reference. |
| 409 | `user_disabled` | The user is disabled. Enable them first if that is what you want. |
| 409 | `user_deleted` | The user was deleted. The `user_id` cannot be used again. |
| 409 | `reference_conflict` | This reference was already used for a different request. Use a new reference. |
| 409 | `balance_changed` | The balance kept changing during a set. Send the same call again shortly. |
| 409 | `host_not_supported` | Your site is not on a Sozee address. Contact Sozee. |
| 422 | `validation_error` | A field is missing or out of range. `fields` names which. Fix the request. |
| 429 | `rate_limited` | Too many calls. Wait for the number of seconds in the `Retry-After` header, then retry. |
| 502 | `upstream_unavailable` | Sozee could not reach a service it needs. Retry with backoff. |
| 503 | `partner_api_disabled` | New sessions and credit changes are paused. Reads and user management still work. Retry later. |
| 503 | `dependency_unavailable` | A service Sozee depends on did not answer. Retry with backoff. |
| 503 | `revocation_incomplete` | The user is blocked but some sessions were not ended yet. Retry the same call. |
| 503 | `usage_unavailable` | Usage reports are not available right now. Retry later. |

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

**Error**

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

### Retry rules

- 429: wait for `Retry-After`, then retry.
- 5xx and network errors: retry with backoff, for example after 1, 2, 4 and 8 seconds.
- Credit calls are always safe to retry with the same `reference`.
- Never retry any other 4xx unchanged. Fix the request first. The one exception is 409 `balance_changed`, which you can send again as it is.

## Limits

| Limit | Value |
|---|---|
| Session calls | 300 a minute per key |
| Credit calls | 600 a minute per key |
| Usage calls | 120 a minute per key |
| Other calls | Not limited today |
| `user_id` | 1 to 128 characters, no control characters |
| `reference` | 1 to 128 characters |
| `amount` | Above 0, at most 2 decimals, at most 1,000,000 per call |
| `balance` (set) | 0 to 1,000,000, at most 2 decimals |
| Usage range | Up to 92 days |

### Not available yet

- Sites on your own domain. Today your site lives on a `sozee.ai` address.
- Webhooks for credits and usage. Read them with the calls on this page.
- A call that lists all your users. Keep your own list of the `user_id` values you send.
- An itemized usage feed. `GET /usage` gives totals per user.
- Teams for your users. Each user works on their own.

## Go-live checklist

- Your key is stored in your secrets manager and used only on your server. It appears in no browser code, app, repository or log.
- Your server calls `https://app.sozee.ai/api/partner/v1` with your `szp_live_` key, and `GET /me` answers your site address.
- Your `/open-sozee` route checks your own login, calls `POST /sessions` on every click and redirects with 302 to `launch_url`.
- You sent Sozee your `signInUrl` (that same route) and your `creditsUrl`, and told us whether you want the Help link or the support chat.
- You send your own stable user id as `user_id`, never an email address unless that is your id.
- New users get credits before or right after their first session.
- Every credit call sends a `reference` taken from your own records, and your code retries timeouts with the same reference.
- Your code handles 409 `user_disabled` and 409 `user_deleted` with a clear message to the user.
- Your logout calls `POST /users/signout`, and closing an account on your side calls `POST /users/disable` or `POST /users/delete`.
- Any made-up test users such as `test_user_1` are deleted.
- You can produce last month's `GET /usage` report, following `next_cursor` until `complete` is `true`, and it matches what you expect to be invoiced.

## Brief for your AI coding agent

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

```text
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](https://app.sozee.ai/api/partner/v1/openapi.json).

### `GET /me`

Check your key and see which partner it belongs to.

**Answer**

| Field | Type | Notes |
|---|---|---|
| `partner_id` | string | Your partner id. |
| `name` | string | Your partner name. |
| `environment` | string | Always `live`. |
| `site_url` | string | Your 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**

| Field | Type | Notes |
|---|---|---|
| `user_id` | string | Your id for the user, 1 to 128 characters, used exactly as sent. |
| `email` | string, optional | Kept for your and our reference. Never shown to the user, used to sign in or emailed. |
| `name` | string, optional | Shown to the user in the studio's account menu. |

**Answer**

| Field | Type | Notes |
|---|---|---|
| `launch_url` | string | Redirect the browser here. Works once. |
| `expires_in` | integer | Seconds the link stays valid: 120. |
| `user` | object | The 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**

| Field | Type | Notes |
|---|---|---|
| `user_id` | query string | Your id for the user, URL-encoded. |

**Answer**

| Field | Type | Notes |
|---|---|---|
| `user_id` | string | Your id for the user. |
| `status` | string | `active`, `disabled` or `deleted`. |
| `created_at` | string | ISO 8601 UTC with milliseconds. Absent for a deleted user. |
| `credits.balance` | number | The 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**

| Field | Type | Notes |
|---|---|---|
| `user_id` | string | Your id for the user, 1 to 128 characters, used exactly as sent. |
| `amount` | number | Above 0, at most 2 decimals, at most 1,000,000. A string such as "50" is refused. |
| `reference` | string | Your own id for this credit operation, 1 to 128 characters. Unique across your account. |

**Answer**

| Field | Type | Notes |
|---|---|---|
| `user_id` | string | Your id for the user. |
| `balance` | number | The balance right after the change. On a replay, the balance now (absent for a deleted user). |
| `amount` | number | The amount added. |
| `reference` | string | Your reference. |
| `replayed` | boolean | `true` 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**

| Field | Type | Notes |
|---|---|---|
| `user_id` | string | Your id for the user, 1 to 128 characters, used exactly as sent. |
| `balance` | number | 0 to 1,000,000, at most 2 decimals. |
| `reference` | string | Your own id for this credit operation, 1 to 128 characters. Unique across your account. |

**Answer**

| Field | Type | Notes |
|---|---|---|
| `user_id` | string | Your id for the user. |
| `balance` | number | The balance right after the change. On a replay, the balance now (absent for a deleted user). |
| `previous_balance` | number | The balance before the change. On a replay, the value from the first time. |
| `reference` | string | Your reference. |
| `replayed` | boolean | `true` 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**

| Field | Type | Notes |
|---|---|---|
| `from` | query, YYYY-MM-DD | First UTC day, included. |
| `to` | query, YYYY-MM-DD | Last UTC day, not included. At most 92 days after `from`. |
| `user_id` | query, optional | Limit the report to one user. |
| `cursor` | query, optional | The `next_cursor` from the previous page. |

**Answer**

| Field | Type | Notes |
|---|---|---|
| `from` | string | As sent. |
| `to` | string | As sent. |
| `complete` | boolean | `false` when more pages follow. |
| `next_cursor` | string or null | Send it as `cursor` to get the next page. `null` when complete. |
| `users` | array | One entry per user: `user_id`, `credits_added`, `credits_spent`, `credits_refunded`, `credits_used`. |
| `totals` | object | The 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**

| Field | Type | Notes |
|---|---|---|
| `user_id` | string | Your id for the user, 1 to 128 characters, used exactly as sent. |

**Answer**

| Field | Type | Notes |
|---|---|---|
| `user_id` | string | Your id for the user. |
| `status` | string | The 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**

| Field | Type | Notes |
|---|---|---|
| `user_id` | string | Your id for the user, 1 to 128 characters, used exactly as sent. |

**Answer**

| Field | Type | Notes |
|---|---|---|
| `user_id` | string | Your id for the user. |
| `status` | string | The 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**

| Field | Type | Notes |
|---|---|---|
| `user_id` | string | Your id for the user, 1 to 128 characters, used exactly as sent. |

**Answer**

| Field | Type | Notes |
|---|---|---|
| `user_id` | string | Your id for the user. |
| `status` | string | The 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**

| Field | Type | Notes |
|---|---|---|
| `user_id` | string | Your id for the user, 1 to 128 characters, used exactly as sent. |

**Answer**

| Field | Type | Notes |
|---|---|---|
| `user_id` | string | Your id for the user. |
| `status` | string | The 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**

| Field | Type | Notes |
|---|---|---|
| (document) | JSON | The 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](mailto:support@sozee.ai) · [White label](https://www.sozee.ai/white-label)
