> ## Documentation Index
> Fetch the complete documentation index at: https://docs.omnichar.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Licenses API: issue, list and revoke

> Issue an Ed25519-signed license for a character, to a named team or as a secret you hand out, with a versioned scope and dates, then list or revoke it. Provider teams only.

A license records that your team lets another team use one of your characters, for a stated use, media and territory, between two dates. Only teams with the provider role can issue or revoke licenses; others get `403`. Ask us to switch it on for your team.

Once issued, the license is enforced: the character appears in the licensee's library, and every run and download with it is checked against the license. See [Using licensed characters](https://docs.omnichar.org/api/licensed-characters.md).

## Issue a license

`POST /v1/licenses` · scope `licenses:write`

```bash theme={null}
curl -s -X POST https://api.omnichar.org/v1/licenses \
  -H "Authorization: Bearer $OMNICHAR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: license-OWND-L-1042" \
  -d '{
    "char_id": "7f1c2a9e-...",
    "licensee_email": "producer@studio.example",
    "scope": {
      "use": "advertising",
      "media": ["image", "video"],
      "territories": ["SE", "NO"],
      "max_generations": 500
    },
    "starts_at": "2026-12-01T00:00:00Z",
    "ends_at": "2027-06-01T00:00:00Z",
    "provider_license_ref": "OWND-L-1042"
  }'
```

```json theme={null}
{
  "key": "lic_live_Q2x8...",
  "env": "production",
  "status": "active",
  "char_id": "7f1c2a9e-...",
  "provider_team_id": "2c0d...",
  "licensee_team_id": "91aa...",
  "provider_license_ref": "OWND-L-1042",
  "scope": { "use": "advertising", "media": ["image", "video"], "territories": ["SE", "NO"], "allow_list": [], "max_generations": 500 },
  "scope_version": 1,
  "starts_at": "2026-12-01T00:00:00.000Z",
  "ends_at": "2027-06-01T00:00:00.000Z",
  "signature": "kR0...",
  "kid": "live-2026-11-09",
  "revoked_reason": null,
  "created_at": "2026-11-09T10:15:02.000Z"
}
```

The character must be yours, in the same environment as the key, and `ready`.

### Naming the licensee

Send at most one of the fields below. Send none to issue a [secret license](#secret-licenses) instead.

| Field | Resolves to |
| - | - |
| `licensee_team_id` | That team. |
| `licensee_team_slug` | The team with that slug. |
| `licensee_email` | The personal workspace of the Omnichar account with that email. |

An unknown team id or slug answers `404` with the code `licensee_not_found`.

An email with no account yet is not refused. The license is created with status `pending` and `licensee_team_id: null`, and you get `license.issued` straight away. The first time that address signs in to Omnichar with Google, the license attaches to the person's personal workspace, becomes `active`, and the licensee gets `license.issued` too. Attaching signs the license again, so its `signature` and `kid` change once, and you get `license.updated` with the new values to replace your stored copy.

### Scope

`scope` is stored with `scope_version: 1`. These fields are checked:

| Field | Type | Checked against |
| - | - | - |
| `use` | string, required | `license_context.use` on every run |
| `media` | list of `image`, `video`, `voice` | the type of output a run makes |
| `territories` | list of strings; `worldwide` matches every territory | `license_context.territory` on every run |
| `allow_list` | list of `surface:cloud`, `surface:api`, `surface:app` and `team:<team id>` entries | where the run comes from and which team runs it; empty allows both |
| `max_generations` | integer, optional | the number of runs the license has allowed so far |

`media` names output types. A license whose `media` lists other words, such as `social`, allows no runs. The `cloud` surface is the Omnichar Cloud dashboard and `api` is any API key; `app` is reserved for the desktop app and matches nothing yet.

Any other field is kept exactly as you send it, so you can record terms we don't model yet.

## Secret licenses

A secret license names no licensee. You get a secret with it, and any Omnichar team that enters the secret holds the license, as many teams as you share it with. Use one per partner: a secret for one studio, another for the next, each with its own scope and dates. Revoking one blocks only the teams that entered it.

Leave out all three licensee fields and add an optional `label`, your own name for who it is for:

```bash theme={null}
curl -s -X POST https://api.omnichar.org/v1/licenses \
  -H "Authorization: Bearer $OMNICHAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "char_id": "7f1c2a9e-...",
    "label": "Studio North",
    "scope": { "use": "advertising", "media": ["image"], "territories": ["worldwide"] },
    "starts_at": "2026-12-01T00:00:00Z",
    "ends_at": "2027-12-01T00:00:00Z"
  }'
```

The answer is the license with `"kind": "secret"`, `"licensee_team_id": null` and one extra field, `secret`, which starts `lsk_live_` (or `lsk_test_` in sandbox). The key and the secret do different jobs:

| | Looks like | Who sees it | What it does |
| - | - | - | - |
| License key | `lic_live_...` | Anyone; it is in the `.char` and in outputs | Verifies the license |
| Secret | `lsk_live_...` | You, and whoever you send it to | Lets a team use the character |

Treat the secret like a password: anyone who has it and the file can generate with the character under the license's terms until you revoke it.

### Share it by email

In the dashboard, **Share via email** on a license sends it from Omnichar with your team's name and logo: a **Generate** button that adds the character to the recipient's account and opens it in one click, a download of the `.char` (link valid 7 days, counted in the license's downloads, and stopped once you revoke), the secret, the terms, and a button to verify the license. The headline names you by your first name, with your verified email beside it, and replies come back to you. Owners and admins set the logo under **Settings → Licensing**. Up to 20 emails an hour per team.

### Get the secret again

`GET /v1/licenses/{key}/secret` · scope `licenses:write`

Returns `{"key": "...", "secret": "lsk_live_..."}` for a secret license your team issued. Every reveal is recorded in your team's audit log.

### The file to send

`POST /v1/licenses/{key}/share-file` · scope `licenses:write`

Returns a download link, valid for 15 minutes, to a copy of the character's `.char` whose manifest carries the signed license block, so it can be [verified offline](https://docs.omnichar.org/api/verify-licenses-offline.md). The file never contains the secret. Send the file and the secret separately.

### Entering a secret

`POST /v1/licenses/redeem` · scope `chars:read`

```bash theme={null}
curl -s -X POST https://api.omnichar.org/v1/licenses/redeem \
  -H "Authorization: Bearer $OMNICHAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"secret":"lsk_live_...","license_key":"lic_live_..."}'
# -> {"license_key":"lic_live_...","char_id":"7f1c2a9e-..."}
```

`license_key` is the key in the `.char` the secret came with (`reserved.license.key` in its manifest). The secret works only with that file's license, so the two have to arrive together.

The caller's team now holds the license: the character is in its [licensed characters](https://docs.omnichar.org/api/licensed-characters.md) and its runs are checked against the license like any licensee's. Entering it again changes nothing. A run can also carry the secret in `license_context.secret`, which enters it on the way, so an API caller need not redeem first. The run must name the licensed character by `char_id`.

A wrong secret, a secret with another license's key, a sandbox secret on a live key and the reverse all answer `404`. A revoked or expired license answers `403` with the reason. Your own team cannot redeem its own secret. Redeeming is limited to 10 tries a minute per team.

You get `license.redeemed` each time a new team enters the secret, with that team's id as `redeemer_team_id`. The license's usage shows every holder's runs; each holder sees only its own.

## List licenses

`GET /v1/licenses` · scope `licenses:read`

```bash theme={null}
curl -s "https://api.omnichar.org/v1/licenses?char_id=$CHAR_ID&role=provider" \
  -H "Authorization: Bearer $OMNICHAR_API_KEY"
```

`role` is `provider` (licenses you issued), `licensee` (licenses you hold) or `any`, the default. The list holds the newest 200.

## Revoke a license

`POST /v1/licenses/{key}/revoke` · scope `licenses:write`

```bash theme={null}
curl -s -X POST https://api.omnichar.org/v1/licenses/$LICENSE_KEY/revoke \
  -H "Authorization: Bearer $OMNICHAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason":"Campaign cancelled"}'
```

A reason is required. The status changes at once, [verify](https://docs.omnichar.org/api/verify.md) answers `revoked` from then on, and both teams get `license.revoked`.

## In the dashboard

A provider team has a **Licenses** page in Omnichar Cloud. It lists every license the team issued, with tabs by status. Each row shows the character, the licensee and the runs used against `max_generations`. Select a license to open its usage, check it on the verify page, or revoke it with a reason. Use the Production and Sandbox switch at the top to see the licenses in each environment.

To issue a secret license from the dashboard, open **Characters**, press the license button on one of your characters, fill in who it is for and the terms, and copy the secret. A license detail on the Licenses page shows the secret again, the file to send, and the license's usage. Characters you built in the dashboard can be licensed this way too; the first license signs the character as yours.

Named licenses are issued through the API.

## Expiry

A license is `expired` from `ends_at`. Verify reports it straight away; the `license.expired` event follows within five minutes.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.