Joan API
Joan is an API-first licensing and retail intelligence platform. Every endpoint is read-only today; writes happen inside the Onbase admin console.
Overview
Joan models four first-class entities — institutions (universities, conferences, bowl games, pro teams), licenses, retailers, and people. Each entity is keyed by a stable UUID plus a URL-safe slug, and carries a codesmap with the entity's identifier in each IP-management platform (CLC, BrandManager360, BrandComply, Exemplar, Affinity, Disney).
An institution (and its licenses) is represented by exactly one IPM at a time — IPMs are competitors, not overlapping systems.
Base URL
All REST endpoints live under /api/v1. The MCP server lives at /api/mcp.
https://www.joanapi.com/api/v1Authentication
Joan uses bearer tokens scoped to an organization. Every token is hashed at rest and only displayed once at mint time — save it in your secret manager immediately.
Rate limits
No hard rate limit is enforced today. Each request is logged per-token in api_request_log— if your usage becomes disruptive you'll hear from us before we add limits. Expect a per-org budget on the order of a few thousand requests per minute when limits do land.
Errors
Errors always return JSON of the shape { error: string }.
| Status | Error code | Meaning |
|---|---|---|
| 401 | unauthorized | Missing, malformed, revoked, or expired token. |
| 404 | not_found | Entity doesn't exist (or was merged). |
| 500 | internal_error | Server-side fault — retry with backoff; check Joan status. |
REST reference
Every endpoint below requires a bearer token. Path params in braces accept either the entity's slug or its CLC code; the /people/{id} endpoint takes a UUID.
/api/v1/Returns a machine-readable list of every endpoint, useful for healthchecks and service discovery.
{
"api": "joan",
"version": "v1",
"org_id": "…",
"endpoints": [
"GET /api/v1/search?q=…&type=institution|retailer|license|person",
"GET /api/v1/institutions?q=&page=",
…
]
}Search
/api/v1/searchFull-text search across institutions, retailers, licenses, and people.
| Param | Type | Description | |
|---|---|---|---|
| q | string | required | Text to search for (trigram match). |
| type | string | optional | Restrict to one of: institution, retailer, license, person. |
| limit | integer | optional | 1–100, default 25. |
curl https://www.joanapi.com/api/v1/search?q=USC \
-H "Authorization: Bearer joan_live_…"{
"query": "USC",
"type": null,
"results": [
{
"type": "institution",
"id": "…",
"name": "University of South Carolina",
"slug": "university-of-south-carolina-usc",
"codes": { "clc": "USC" }
}
]
}List institutions
/api/v1/institutions| Param | Type | Description | |
|---|---|---|---|
| q | string | optional | Fuzzy name filter. |
| page | integer | optional | 1-indexed; default 1. |
{
"page": 1,
"pageSize": 25,
"total": 658,
"items": [
{
"id": "…",
"name": "Alabama A&M University",
"slug": "alabama-am-university-aam",
"kind": "university",
"codes": { "clc": "AAM" },
"license_count": 3
}
]
}Get institution
/api/v1/institutions/{key}Accepts the slug or any IPM code (USC, university-of-south-carolina-usc, etc.).
{
"id": "…",
"name": "University of South Carolina",
"slug": "university-of-south-carolina-usc",
"kind": "university",
"merged_into": null,
"codes": {
"clc": {
"code": "USC",
"metadata": {
"standard_rate": 0.12,
"advance_apparel": 2500,
"advance_non_apparel": 1000
}
}
},
"licenses": [
{ "id": "…", "description": "…", "license_type": "single", "codes": { "clc": "00001" } }
]
}List an institution's licenses
/api/v1/institutions/{key}/licensescurl https://www.joanapi.com/api/v1/institutions/USC/licenses \
-H "Authorization: Bearer joan_live_…"{
"institution_id": "…",
"institution_name": "University of South Carolina",
"licenses": [
{ "id": "…", "description": "…", "license_type": "single", "codes": { "clc": "00001" } }
]
}List an institution's people
/api/v1/institutions/{key}/people{
"institution_id": "…",
"people": [
{
"role": "Licensing Director",
"is_primary": true,
"person": {
"id": "…",
"full_name": "Jane Doe",
"email": "jane@usc.edu",
"phone": "+1-555-…",
"title": "Sr. Director, Licensing",
"linkedin_url": "https://www.linkedin.com/in/janedoe"
}
}
]
}List retailers
/api/v1/retailers| Param | Type | Description | |
|---|---|---|---|
| q | string | optional | Fuzzy name filter. |
| type | string | optional | CLC retailer_type filter (e.g. DPT, SGSS, GDC). |
| page | integer | optional | 1-indexed; default 1. |
{
"page": 1,
"pageSize": 25,
"total": 115831,
"items": [
{
"id": "…",
"name": "Scheels",
"slug": "scheels-schels",
"codes": {
"clc": { "code": "SCHELS", "retailer_type": "SGSS", "is_global": true }
}
}
]
}Get retailer
/api/v1/retailers/{key}{
"id": "…",
"name": "Scheels",
"slug": "scheels-schels",
"codes": {
"clc": {
"code": "SCHELS",
"metadata": { "retailer_type": "SGSS", "is_global": true }
}
}
}List a retailer's approved licenses
/api/v1/retailers/{key}/licensesPopulated from IPM platform data — empty until the license↔retailer junction is sourced.
{
"retailer_id": "…",
"approved_licenses": [
{
"source": "clc",
"license": { "id": "…", "description": "…", "license_type": "single", "institution_id": "…" }
}
]
}List a retailer's people
/api/v1/retailers/{key}/people{
"retailer_id": "…",
"people": [
{
"role": "Buyer",
"is_primary": true,
"person": { "id": "…", "full_name": "…", "email": "…", "title": "…" }
}
]
}List licenses
/api/v1/licenses| Param | Type | Description | |
|---|---|---|---|
| q | string | optional | Fuzzy description filter. |
| type | string | optional | single or multi. |
| page | integer | optional | 1-indexed; default 1. |
Get license
/api/v1/licenses/{key}{key} is the CLC license code or the license UUID.
{
"id": "…",
"description": "…",
"license_type": "single",
"codes": { "clc": "00001" },
"institution": {
"id": "…",
"slug": "university-of-south-carolina-usc",
"name": "University of South Carolina",
"kind": "university"
}
}Get person
/api/v1/people/{id}{id}must be a UUID. People don't have external codes — discover them through /institutions/{key}/people or /retailers/{key}/people.
{
"id": "…",
"full_name": "Jane Doe",
"email": "jane@usc.edu",
"phone": "+1-555-…",
"title": "Sr. Director, Licensing",
"linkedin_url": "…",
"source": "csv",
"institution_roles": [
{
"role": "Licensing Director",
"is_primary": true,
"institutions": { "id": "…", "slug": "…", "name": "…", "kind": "university" }
}
],
"retailer_roles": []
}MCP server
The MCP server wraps the same data with the Model Context Protocol so LLM tools can call Joan natively. Transport is Streamable HTTP; auth is the same bearer token as REST.
Claude Desktop
Edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows) and add the Joan server:
{
"mcpServers": {
"joan": {
"url": "https://www.joanapi.com/api/mcp",
"headers": {
"Authorization": "Bearer joan_live_xxxxxxxxxxxx"
}
}
}
}Restart Claude Desktop. The hammer icon in the composer will show the Joan tools.
Cursor
In Cursor: Settings → MCP → Add new MCP server. Paste this config:
{
"mcpServers": {
"joan": {
"url": "https://www.joanapi.com/api/mcp",
"headers": {
"Authorization": "Bearer joan_live_xxxxxxxxxxxx"
}
}
}
}Other MCP clients
Any client that supports Streamable-HTTP MCP transport will work — point it at the URL above and supply the bearer header. If your client only supports stdio, wrap the URL with mcp-proxy.
Tool reference
All tools are read-only. key accepts the slug or CLC code; id is always a UUID.
| Tool | Arguments | What it returns |
|---|---|---|
| joan_search | query, type?, limit? | Unified search across all four entity types. |
| joan_get_institution | key | Institution + all IPM codes + all its licenses. |
| joan_list_institution_licenses | key | Just the licenses for one institution. |
| joan_list_institution_people | key | People attached to an institution, with roles. |
| joan_get_retailer | key | Retailer + all IPM codes + metadata. |
| joan_list_retailer_people | key | People attached to a retailer (buyers, etc.). |
| joan_list_retailer_licenses | key | Licenses approved at this retailer. |
| joan_get_license | key | License + institution + all IPM codes. |
| joan_get_person | id | Person + every institution and retailer role they hold. |
Examples
curl
TOKEN="joan_live_xxxxxxxxxxxx"
# search
curl -s "https://www.joanapi.com/api/v1/search?q=USC" \
-H "Authorization: Bearer $TOKEN" | jq
# get one institution
curl -s "https://www.joanapi.com/api/v1/institutions/USC" \
-H "Authorization: Bearer $TOKEN" | jq '.name, .codes'
# list a retailer's people
curl -s "https://www.joanapi.com/api/v1/retailers/SCHELS/people" \
-H "Authorization: Bearer $TOKEN" | jqTypeScript
const TOKEN = process.env.JOAN_TOKEN!;
const BASE = "https://www.joanapi.com/api/v1";
async function joan<T>(path: string): Promise<T> {
const res = await fetch(`${BASE}${path}`, {
headers: { Authorization: `Bearer ${TOKEN}` },
});
if (!res.ok) throw new Error(`joan ${res.status} ${path}`);
return res.json() as Promise<T>;
}
const inst = await joan<{ name: string; codes: Record<string, unknown> }>(
"/institutions/USC",
);
console.log(inst.name, inst.codes);Python
import os, requests
TOKEN = os.environ["JOAN_TOKEN"]
BASE = "https://www.joanapi.com/api/v1"
def joan(path: str):
r = requests.get(BASE + path, headers={"Authorization": f"Bearer {TOKEN}"})
r.raise_for_status()
return r.json()
inst = joan("/institutions/USC")
print(inst["name"], inst["codes"])