Skip to content

Integrate your app

Prosopia is a site-to-site broker for researcher profiles. One canonical profile per researcher lives here, owned by that person; your application reads it, ranks against it, and contributes to it through a single API key — without ever holding the researcher’s login. This guide is for the developer wiring another service (a lab portal, a matcher, an authoring tool) into Prosopia.

It answers, in order: how your app authenticates, what it is allowed to see, how your users reach their own access, how you control identity, and how you pull and push information. For the per-endpoint contract, see the API reference; for the ideas behind access and visibility, see Core concepts.

Base URLs: https://prosopia.databio.org (public; profiles.databio.org is the same app). Inside the lab cluster, services reach it at http://prosopia:8200.

  • A profile is keyed on a canonical rid — a researcher’s ORCID iD, or a minted local: id for someone with no ORCID.
  • A person owns their profile through an ORCID login. Your app never performs that login and never becomes an owner.
  • Your app is a consumer principal: it authenticates with an app key that carries scopes (what operations it may perform) and a viewer tier (how much of each profile it may see). Both are set when the key is minted.
  • You contribute in two distinct ways: push a canonical document (you are the system of record for that researcher), or lay a private overlay over someone else’s canonical profile (you augment it; the owner decides whether to adopt your changes).

Everything below is a consequence of these four facts.

Your app authenticates with a bearer token on every request:

Authorization: Bearer rpk_live_xxxxxxxxxxxxxxxx

An operator mints the key for your consumer from the admin panel (POST /api/admin/consumers/{id}/keys); the plaintext is shown once, and only its hash is stored. Ask the operator for a key with the scopes and tier your integration needs.

Two key kinds — use the app key. App keys are prefixed rpk_ and carry app scopes. Agent keys (rpa_) are a different mechanism: a person mints one against their own profile and hands it to a narrow tool, to read that profile (the shared read scope) or to edit specific fields. A site-to-site integration uses an rpk_ app key. An rpa_ key holding only agent scopes is refused with 403 at any other app-scoped route.

The difference that matters is reach. An app key’s minted tier IS its authorization, and it applies to every profile the key can see. An agent key carries no tier of its own: it reads through the grant its owner wrote, so it reaches one profile and is worth an anonymous request everywhere else. If your application serves many researchers and you hold one credential for all of them, you want an app key. If each of your users brings their own profile — pastes in a token and an id — you want them to bring you an agent key, and you never hold a credential that spans them.

Scopes — request only what you use:

ScopeLets your app
readResolve a viewer tier for reads (reads themselves are not scope-gated; see below)
matchCall /match and per-profile /search
personaCall the generative endpoints (ask, review, innovate, riff)
pushCreate or replace canonical profiles, and download archives

Viewer tierpublic, internal, or restricted, minted onto the key. This is the ceiling on what your app sees, applied to every read. Most lab integrations want internal; restricted is reserved for trusted services.

  • 401 — missing, malformed, unknown, or revoked key.
  • 403 — the key lacks the scope for this route. The body is {"detail": "consumer '<name>' lacks scope '<scope>'"}.

(The richer {error, required, granted, missing, hint} body you may see elsewhere belongs to the agent-key edit path, not to app-scope calls.)

Reads are not gated by a scope. Instead, every read is projected through your key’s viewer tier, artifact by artifact, on every request. Your app sees exactly what its tier allows and no more.

  • An artifact above your tier is returned as 404 — indistinguishable from an artifact that does not exist. You cannot probe for what you may not see.
  • Two hard floors return 403 for everyone, at every tier: paper full text, and the internal cache/ and .keys/ prefixes. No key, grant, or operator token lifts these.
  • Unpublished profiles are floored at internal. A public-tier key (or an anonymous caller) sees only published profiles; an internal- or restricted-tier consumer keeps seeing lab profiles whether or not the owner has published them.

Read the tier back from the response headers:

  • X-RP-Viewer-Tier — the tier your request resolved to. Present on every response, including refusals.
  • X-RP-Effective-Tier — on a single-artifact read, the tier that artifact was projected to.
  • X-RP-Archive-Tier, X-RP-Archive-Digest, X-RP-Profile-Level — on the push-scoped archive download.

Discover the catalog — three read routes tell you what exists and what you may fetch:

Terminal window
curl -H "Authorization: Bearer $RPK" \
https://prosopia.databio.org/api/v1/profiles # listing (only profiles you can see)
curl -H "Authorization: Bearer $RPK" \
https://prosopia.databio.org/api/v1/registry.json # bundle of cards with absolute base URLs
curl -H "Authorization: Bearer $RPK" \
https://prosopia.databio.org/api/v1/profiles/jane-doe # detail

The detail response is your inventory for one profile:

{
"slug": "jane-doe",
"rid": "0000-0002-1825-0097",
"metadata": { "...": "..." },
"expertise": { "...": "..." },
"soul": "...",
"manifest": ["metadata", "expertise", "soul", "papers/…"],
"withheld": [""],
"content_hash": "sha256:…"
}

manifest lists the artifacts you may retrieve; a body you may not see comes back null rather than erroring the whole request. content_hash is the value you use for safe writes (see Push information).

Your app authenticates as itself, not as each of your users. There is no way to mint a per-user session from your key, and that is deliberate — Prosopia never lets a broker impersonate a person. So “what your users can access” resolves through three mechanisms, not a proxied login:

  1. Your key’s tier is the floor for every user of your app. If your key is internal, all your users read profiles at internal.

  2. Owners share with your consumer. A person can give your consumer a read (or write) permission on everything they hold — their profile and the lenses they wrote — from the Access tab on their profile, or with POST /api/manage/permissions and {"principal_kind": "consumer", "principal_ref": "<your-consumer-id>", "grant_type": "read"}. Per the viewer-tier table, a permission reads that person’s data whole. This is how an owner opts your app into the deeper parts of their profile.

  3. Person-links map your users to profiles (next section), so your app can answer “which canonical profile is my user #4821?” without asking the user.

If a flow genuinely needs a person to act as themselves — to claim a profile, publish it, or make an owner-only edit — your site sends that person to Prosopia’s ORCID login (/api/manage/login) and lets them act in their own session. Your key cannot and should not stand in for them.

The join key between your world and Prosopia’s is the rid. Get it right and everything else lines up.

Resolve before you create. Before onboarding a researcher, ask whether a canonical profile already exists:

Terminal window
curl "https://prosopia.databio.org/api/v1/identity/resolve?orcid=0000-0002-1825-0097"
{ "slug": "jane-doe", "rid": "0000-0002-1825-0097", "claimed": true, "owner_orcid": "0000-0002-1825-0097" }

404 means the identity is not visible to you — anonymous callers see only published, publicly visible profiles, so an unpublished profile 404s here too. Do NOT treat a 404 as proof the person is absent or as a green light to onboard them; that decision belongs to the authoritative resolver, POST /api/v1/identity/resolve (send your consumer key with the resolve scope), which matches or mints exactly once. Authenticate the GET with your rpk_ key to see unpublished profiles. claimed tells you whether a person has taken ownership. You can resolve by rid= (ORCID or local:) or orcid=.

Do not invent local: ids yourself. For a researcher with no ORCID, let Prosopia mint the id at push time by opting in — either a JSON field or a query flag on the push:

Terminal window
curl -H "Authorization: Bearer $RPK" \
-H "Content-Type: application/json" \
-X PUT https://prosopia.databio.org/api/v1/profiles/rivka-cohen \
-d '{ "name": "Rivka Cohen", "mintLocalRid": true, "...": "..." }'

Prosopia returns the minted rid (form local:<slug>-<hex6>) in the response; record it as your permanent join key. The rules are strict, so minting is never accidental:

  • No rid in the document and no opt-in → 400 (“supply an ORCID rid, or pass mintLocalRid=true / ?mint=local”).
  • Opt-in with an empty name400.
  • A rid already present and the mint flag set → 400 (no silent overwrite).

A local:-rid profile stays unclaimed — its rid is not an ORCID, so no ORCID login self-claims it. It is a lab-visible identity record, not a published web profile.

Remember the mapping with person-links. Bind your own external id to the canonical profile so you never re-resolve:

Terminal window
curl -H "Authorization: Bearer $RPK" \
-H "Content-Type: application/json" \
-X POST https://prosopia.databio.org/api/manage/consumers/your-app/person-links \
-d '{ "external_id": "4821", "rid": "0000-0002-1825-0097" }'

Look one up with the external id in the path:

Terminal window
curl -H "Authorization: Bearer $RPK" \
https://prosopia.databio.org/api/manage/consumers/your-app/person-links/4821
# -> { "consumer_id": "your-app", "external_id": "4821", "slug": "jane-doe", "rid": "…" }

You must call these as the named consumer; another consumer’s key gets 403.

Read one profile or the whole catalog — the routes shown under Know what you can retrieve. Fetch a single artifact directly, and read X-RP-Effective-Tier to see the tier it came back at:

Terminal window
curl -H "Authorization: Bearer $RPK" \
https://prosopia.databio.org/api/v1/profiles/jane-doe/content/expertise

Match across profiles (scope match) — semantic ranking, the core brokerage read:

Terminal window
curl -H "Authorization: Bearer $RPK" \
-H "Content-Type: application/json" \
-X POST https://prosopia.databio.org/api/v1/match \
-d '{ "query": "single-cell chromatin accessibility", "k": 5 }'
{
"matches": [
{
"slug": "jane-doe",
"name": "Jane Doe",
"rid": "0000-0002-1825-0097",
"orcid": "0000-0002-1825-0097",
"score": 0.83,
"evidence": {
"centroid_score": 0.81,
"top_papers": ["doe2024atlas"],
"overlapping_topics": ["chromatin", "single-cell"],
"top_chunks": []
}
}
]
}

Request fields: query (required), k (default 5), prefilter (10), require_topics (list, optional), diversify (true), lambda_ (0.5), topk_chunks (5), normalize (true), include_chunks (false — set true to populate evidence.top_chunks). Results you may not see at your tier are dropped from the ranking.

Matchability depends on an embedding index. A profile is rankable only once it carries a built embedding index. A profile you created with a JSON push (next section) has no index (indexed: false) and will not appear in /match until it is enriched by a full-bundle push or a build. If the deployment has no embeddings support installed, or no profile is indexed yet, /match returns 503.

Ask a profile (scope persona) — generative endpoints grounded in one profile’s corpus: POST /api/v1/profiles/{slug}/ask, .../review, .../innovate, .../riff. ask and review return grounded text with citations; innovate and riff return structured idea lists. See the API reference for each body.

There are two ways to contribute, and choosing correctly is the most important integration decision.

Upsert the canonical document (you are the source of record)

Section titled “Upsert the canonical document (you are the source of record)”

Use this when your app authors the researcher’s profile — you hold the truth about their identity and expertise. It is a create-or-replace on the canonical document, scope push:

Terminal window
curl -H "Authorization: Bearer $RPK" \
-H "Content-Type: application/json" \
-X PUT https://prosopia.databio.org/api/v1/profiles/jane-doe \
-d '{ "rid": "0000-0002-1825-0097", "name": "Jane Doe", "...": "…the profile.jsonld document…" }'

The body is a full profile document (the profile.jsonld shape). Success is 200 with:

{ "slug": "jane-doe", "rid": "0000-0002-1825-0097", "name": "Jane Doe", "level": "lite", "indexed": false }

Two things to know:

  • Content-Type: application/json selects this path. Anything else — or a missing header — is treated as a gzipped-tarball bundle upload, the older path that carries papers and a prebuilt embedding index. Send the JSON content type explicitly.
  • JSON upsert writes the document only. It does not add or remove sources/ (papers) or cache/ (embeddings) artifacts. So it establishes identity and expertise cheaply, but the profile is not matchable until a bundle push or a build adds an index. The division of labor is the point: your app owns identity, Prosopia owns enrichment.

Write safely with If-Match. To avoid clobbering a concurrent edit, send the content_hash you last read:

Terminal window
curl -H "Authorization: Bearer $RPK" \
-H "Content-Type: application/json" \
-H "If-Match: sha256:abcd…" \
-X PUT https://prosopia.databio.org/api/v1/profiles/jane-doe \
-d '{ "...": "…" }'

A stale hash returns 409 with the current value in the X-RP-Content-Hash response header — re-read, merge, retry. Omitting If-Match is last-writer-wins. On success, dateModified is stamped automatically, and only when the content actually changed.

Overlay someone else’s profile (you augment; the owner decides)

Section titled “Overlay someone else’s profile (you augment; the owner decides)”

Use this when you do not own the record — you want to add or correct fields on a profile some other party is the source of record for. An overlay is a consumer-private patch keyed to (profile rid, your consumer). Only your app sees the merged result; everyone else sees the canonical profile untouched.

Terminal window
curl -H "Authorization: Bearer $RPK" \
-H "Content-Type: application/json" \
-X PUT https://prosopia.databio.org/api/v1/profiles/jane-doe/overlay/metadata \
-d '{ "patch": { "affiliation": "…as your system sees it…" } }'

Companion routes: PUT .../overlay/soul ({"soul": <str|null>}), PATCH .../overlay/visibility, GET .../overlay (add ?create=merged to get the merged view when no overlay exists yet), and DELETE .../overlay. Writes return {overlay, merged, stale}. Each write records the canonical base_hash it was authored against; a later canonical edit flips your overlay to stale: true, your signal to reconcile. Overlays are consumer-only — a person session gets 403.

Your overlay is visible to the owner as a suggestion. The owner can accept it (your patch is merged into the canonical profile) or dismiss it (your private overlay stays, canonical is untouched). This is how a broker proposes improvements without seizing authorship.

StatusMeaning
400Bad request — invalid slug, missing/invalid rid or mint opt-in, empty mint name
401Missing, malformed, unknown, or revoked key
403Lacks the required scope; or a hard-floor artifact (full text, cache/, .keys/); or a person session on a consumer-only route
404No such profile, or an artifact above your viewer tier (the two are indistinguishable)
409If-Match conflict (see X-RP-Content-Hash), or a write conflict
422The pushed JSON document failed validation
503/match unavailable — no embeddings support, or no profile is indexed yet