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.
The mental model
Section titled “The mental model”- A profile is keyed on a canonical
rid— a researcher’s ORCID iD, or a mintedlocal: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.
Get a key and authenticate
Section titled “Get a key and authenticate”Your app authenticates with a bearer token on every request:
Authorization: Bearer rpk_live_xxxxxxxxxxxxxxxxAn 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:
| Scope | Lets your app |
|---|---|
read | Resolve a viewer tier for reads (reads themselves are not scope-gated; see below) |
match | Call /match and per-profile /search |
persona | Call the generative endpoints (ask, review, innovate, riff) |
push | Create or replace canonical profiles, and download archives |
Viewer tier — public, 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.
What can go wrong
Section titled “What can go wrong”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.)
Know what you can retrieve
Section titled “Know what you can retrieve”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
403for everyone, at every tier: paper full text, and the internalcache/and.keys/prefixes. No key, grant, or operator token lifts these. - Unpublished profiles are floored at
internal. Apublic-tier key (or an anonymous caller) sees only published profiles; aninternal- orrestricted-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:
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 URLscurl -H "Authorization: Bearer $RPK" \ https://prosopia.databio.org/api/v1/profiles/jane-doe # detailThe 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).
Let your users reach their own access
Section titled “Let your users reach their own access”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:
-
Your key’s tier is the floor for every user of your app. If your key is
internal, all your users read profiles atinternal. -
Owners share with your consumer. A person can give your consumer a
read(orwrite) permission on everything they hold — their profile and the lenses they wrote — from the Access tab on their profile, or withPOST /api/manage/permissionsand{"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. -
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.
Control identity
Section titled “Control identity”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:
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:
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
ridin the document and no opt-in →400(“supply an ORCID rid, or passmintLocalRid=true/?mint=local”). - Opt-in with an empty
name→400. - A
ridalready 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:
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:
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.
Pull information
Section titled “Pull information”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:
curl -H "Authorization: Bearer $RPK" \ https://prosopia.databio.org/api/v1/profiles/jane-doe/content/expertiseMatch across profiles (scope match) — semantic ranking, the core
brokerage read:
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.
Push information
Section titled “Push information”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:
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/jsonselects 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) orcache/(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:
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.
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.
Errors at a glance
Section titled “Errors at a glance”| Status | Meaning |
|---|---|
400 | Bad request — invalid slug, missing/invalid rid or mint opt-in, empty mint name |
401 | Missing, malformed, unknown, or revoked key |
403 | Lacks the required scope; or a hard-floor artifact (full text, cache/, .keys/); or a person session on a consumer-only route |
404 | No such profile, or an artifact above your viewer tier (the two are indistinguishable) |
409 | If-Match conflict (see X-RP-Content-Hash), or a write conflict |
422 | The pushed JSON document failed validation |
503 | /match unavailable — no embeddings support, or no profile is indexed yet |
Related
Section titled “Related”- API reference — every endpoint, field, and header.
- Core concepts — identity, ownership, and the tier engine.
- Share your profile — the owner’s side of granting your app access.