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

# API quickstart: sandbox character to license

> Use sandbox keys to build a test character, license it to another team, generate and download as the licensee, read the usage, then revoke and see the next run blocked.

This walks through the whole API with a sandbox key. Nothing in sandbox runs on a GPU or spends your build balance, so you can repeat it as often as you like.

You need:

* A team on Omnichar Cloud where you are an owner, admin or developer.
* A sandbox key (`sk_test_...`) from **Settings → API keys**, with your team selected. See [Authentication](https://docs.omnichar.org/api/authentication.md).
* A second team to license the character to, with its slug and a sandbox key of its own with the `runs:write` scope. Licenses are issued by provider teams; ask us to switch the provider flag on for your team.

```bash theme={null}
export OMNICHAR_API_KEY=sk_test_...
export LICENSEE_KEY=sk_test_...   # the second team's key
export API=https://api.omnichar.org/v1
```

**1. Check the key**

```bash theme={null}
curl -s $API/team -H "Authorization: Bearer $OMNICHAR_API_KEY"
```

The answer names your team and `"env": "sandbox"`. If it says `production`, you are using a live key.

**2. Upload four face photos**

A character needs 4 to 12 face photos. For each one, ask for an upload link, then send the bytes to it. `bytes` must be the file's exact size:

```bash theme={null}
curl -s -X POST $API/uploads \
  -H "Authorization: Bearer $OMNICHAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"kind":"image","contentType":"image/png","bytes":'$(wc -c < face-1.png)',"name":"face-1.png"}'
# -> {"upload_key":"inputs/...","uploadUrl":"https://...","headers":{"content-type":"image/png"},...}

export UPLOAD_URL='<uploadUrl from the answer>'
curl -s -X PUT "$UPLOAD_URL" -H "content-type: image/png" --data-binary @face-1.png
```

Keep each `upload_key`: it is what you pass as a reference. An upload link lasts 10 minutes. Repeat for `face-2.png` to `face-4.png`. [Uploads](https://docs.omnichar.org/api/uploads.md) has the limits, the voice clip and a script that uploads a whole folder.

**3. Create a character**

```bash theme={null}
curl -s -X POST $API/chars \
  -H "Authorization: Bearer $OMNICHAR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: create-subject-001" \
  -d '{
    "subject_id": "subject-001",
    "references": { "face": ["inputs/...1.png", "inputs/...2.png", "inputs/...3.png", "inputs/...4.png"] },
    "description": "Calm presenter"
  }'
# -> {"id":"7f1c...","status":"queued","version":1}

export CHAR_ID='<id from the answer>'
```

Replace the four `inputs/...` values with your own upload keys.

**4. Wait for char.ready**

In sandbox the build finishes about 30 seconds later. If your team has a [webhook](https://docs.omnichar.org/api/webhooks.md) for `char.ready`, it arrives then. Without one, poll:

```bash theme={null}
curl -s $API/chars/$CHAR_ID -H "Authorization: Bearer $OMNICHAR_API_KEY"
# -> {"status":"ready","download_url":"https://...", ...}
```

**5. Issue a license**

```bash theme={null}
curl -s -X POST $API/licenses \
  -H "Authorization: Bearer $OMNICHAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "char_id": "'$CHAR_ID'",
    "licensee_team_slug": "their-team-slug",
    "scope": { "use": "advertising", "media": ["image"], "territories": ["SE"] },
    "starts_at": "2026-11-01T00:00:00Z",
    "ends_at": "2027-11-01T00:00:00Z"
  }'
# -> {"key":"lic_test_...","status":"active","signature":"...","kid":"...", ...}

export LICENSE_KEY='<key from the answer>'
```

Pick a `starts_at` that has already passed, so verify answers `valid` straight away.

**6. Verify it**

Verification needs no API key, but it needs the license's `signature`, which a verifier reads from the `.char` the license came in. Here you take it from the issue answer:

```bash theme={null}
export SIG='<signature from the issue answer>'
curl -s "$API/licenses/$LICENSE_KEY/verify?signature=$SIG&territory=SE&media=image"
# -> {"status":"valid","env":"sandbox","test":true,"in_scope":true, ...}
```

**7. Generate as the licensee**

The licensee names the character by `char_id` and says how it will use it:

```bash theme={null}
curl -s -X POST $API/runs \
  -H "Authorization: Bearer $LICENSEE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "default-flux-2-klein-4b",
    "characters": { "character": { "char_id": "'$CHAR_ID'" } },
    "license_context": { "use": "advertising", "territory": "SE" }
  }'
# -> {"runId":"0b6e...","status":"queued"}

export RUN_ID='<runId from the answer>'
```

In sandbox the run renders a placeholder about 10 seconds later. Its output carries the license in its metadata:

```bash theme={null}
curl -s $API/runs/$RUN_ID/outputs -H "Authorization: Bearer $LICENSEE_KEY"
```

A `usage.generated` event goes to both teams. Your own report shows it too:

```bash theme={null}
curl -s $API/licenses/$LICENSE_KEY/usage -H "Authorization: Bearer $OMNICHAR_API_KEY"
# -> {"items":[{"result":"allowed","output_count":1, ...}],"totals":{"allowed":1, ...}, ...}
```

**8. Download the licensee's copy**

```bash theme={null}
curl -s -X POST $API/licenses/$LICENSE_KEY/download -H "Authorization: Bearer $LICENSEE_KEY"
# -> {"download_url":"https://...","version":1, ...}

curl -s -o licensed.char '<download_url from the answer>'
unzip -p licensed.char manifest.json | jq .reserved.license
# -> {"key":"lic_test_...","signature":"...","licensee_team_id":"...", ...}
```

[The license in a .char](https://docs.omnichar.org/api/char-license-format.md) shows how to check that block offline.

**9. Revoke it and verify again**

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

curl -s "$API/licenses/$LICENSE_KEY/verify?signature=$SIG"
# -> {"status":"revoked", ...}
```

Both teams' `license.revoked` webhooks fire at the same time. The licensee's next run and download are refused:

```bash theme={null}
curl -s -X POST $API/licenses/$LICENSE_KEY/download -H "Authorization: Bearer $LICENSEE_KEY"
# -> {"error":{"code":"forbidden","details":{"reason":"license_revoked", ...}, ...}}
```

and you get `usage.blocked` for each refusal.

## Next steps

* [Characters](https://docs.omnichar.org/api/characters.md) covers body photos, a voice clip, retraining and the consent receipt.
* [Licenses](https://docs.omnichar.org/api/licenses.md) covers scope, naming the licensee and listing.
* [Using licensed characters](https://docs.omnichar.org/api/licensed-characters.md) covers what is checked and what a licensee can do.
* [Verify webhook signatures](https://docs.omnichar.org/api/verify-webhooks.md) before you act on an event.


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