v1 · Stable

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.

Two ways to consume Joan
REST API — standard HTTPS + JSON for scripts, backends, and any language with an HTTP client.
MCP server — native tools for Claude Desktop, Cursor, and any Model Context Protocol client. Same data, same auth, shaped for LLM tool-calling.

Base URL

All REST endpoints live under /api/v1. The MCP server lives at /api/mcp.

http
https://www.joanapi.com/api/v1

Authentication

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.

1. Get a token
Each token belongs to exactly one organization and starts with the prefix joan_live_. If you don't have access yet, request it here. Onbase admins mint tokens from /admin/orgs.
2. Send it as a bearer
Pass the token in the Authorization header on every request.
http
GET https://www.joanapi.com/api/v1/institutions?q=USC HTTP/1.1
Authorization: Bearer joan_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Scopes
Tokens ship with the read scope by default. Writes are not exposed over the public API — all mutations happen in the admin console.

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

StatusError codeMeaning
401unauthorizedMissing, malformed, revoked, or expired token.
404not_foundEntity doesn't exist (or was merged).
500internal_errorServer-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.

GET/api/v1/

Returns a machine-readable list of every endpoint, useful for healthchecks and service discovery.

json
{
  "api": "joan",
  "version": "v1",
  "org_id": "…",
  "endpoints": [
    "GET /api/v1/search?q=…&type=institution|retailer|license|person",
    "GET /api/v1/institutions?q=&page=",
    …
  ]
}

List institutions

GET/api/v1/institutions
ParamTypeDescription
qstringoptionalFuzzy name filter.
pageintegeroptional1-indexed; default 1.
json
{
  "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

GET/api/v1/institutions/{key}

Accepts the slug or any IPM code (USC, university-of-south-carolina-usc, etc.).

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

GET/api/v1/institutions/{key}/licenses
bash
curl https://www.joanapi.com/api/v1/institutions/USC/licenses \
  -H "Authorization: Bearer joan_live_…"
json
{
  "institution_id": "…",
  "institution_name": "University of South Carolina",
  "licenses": [
    { "id": "…", "description": "…", "license_type": "single", "codes": { "clc": "00001" } }
  ]
}

List an institution's people

GET/api/v1/institutions/{key}/people
json
{
  "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

GET/api/v1/retailers
ParamTypeDescription
qstringoptionalFuzzy name filter.
typestringoptionalCLC retailer_type filter (e.g. DPT, SGSS, GDC).
pageintegeroptional1-indexed; default 1.
json
{
  "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

GET/api/v1/retailers/{key}
json
{
  "id": "…",
  "name": "Scheels",
  "slug": "scheels-schels",
  "codes": {
    "clc": {
      "code": "SCHELS",
      "metadata": { "retailer_type": "SGSS", "is_global": true }
    }
  }
}

List a retailer's approved licenses

GET/api/v1/retailers/{key}/licenses

Populated from IPM platform data — empty until the license↔retailer junction is sourced.

json
{
  "retailer_id": "…",
  "approved_licenses": [
    {
      "source": "clc",
      "license": { "id": "…", "description": "…", "license_type": "single", "institution_id": "…" }
    }
  ]
}

List a retailer's people

GET/api/v1/retailers/{key}/people
json
{
  "retailer_id": "…",
  "people": [
    {
      "role": "Buyer",
      "is_primary": true,
      "person": { "id": "…", "full_name": "…", "email": "…", "title": "…" }
    }
  ]
}

List licenses

GET/api/v1/licenses
ParamTypeDescription
qstringoptionalFuzzy description filter.
typestringoptionalsingle or multi.
pageintegeroptional1-indexed; default 1.

Get license

GET/api/v1/licenses/{key}

{key} is the CLC license code or the license UUID.

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

GET/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.

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

Connection details
URL https://www.joanapi.com/api/mcp
Transport Streamable HTTP
Auth Authorization: Bearer joan_live_…

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:

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

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

ToolArgumentsWhat it returns
joan_searchquery, type?, limit?Unified search across all four entity types.
joan_get_institutionkeyInstitution + all IPM codes + all its licenses.
joan_list_institution_licenseskeyJust the licenses for one institution.
joan_list_institution_peoplekeyPeople attached to an institution, with roles.
joan_get_retailerkeyRetailer + all IPM codes + metadata.
joan_list_retailer_peoplekeyPeople attached to a retailer (buyers, etc.).
joan_list_retailer_licenseskeyLicenses approved at this retailer.
joan_get_licensekeyLicense + institution + all IPM codes.
joan_get_personidPerson + every institution and retailer role they hold.

Examples

curl

bash
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" | jq

TypeScript

typescript
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

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"])

Questions or issues? Reach out to the Onbase team. This page is generated from the live API — it always matches what's deployed.