Show Sifa profiles in your app
How to read a headline, a current role, an employer, and skills from a Sifa profile and render them in your own app. Public data, no account, no API key.
If you build on the Atmosphere, you can show the professional facts of a person where they help: a maintainer on a package page, a developer on a git forge, a contributor in a byline. Those facts are the current role, the employer, and the listed skills. The data is public and portable, so you read it directly. There is no Sifa account to create, no API key to request, and nothing to ask permission for.
There are three ways in, from least to most work. Pick the one that fits your stack.
Option 1: call the profile query
Sifa's AppView exposes an XRPC query that returns the aggregated public profile. Call it from your server with a username or a DID:
curl "https://api.sifa.id/xrpc/id.sifa.getProfileView?actor=gui.do"The response is JSON. The fields most apps want:
{
"did": "did:plc:45uheisi25szrjvjurfpritx",
"handle": "gui.do",
"displayName": "Guido Jansen",
"headline": "Founder at Singi Labs",
"positions": [
{
"title": "Founder",
"company": "Singi Labs",
"entityName": "Singi Labs",
"startedAt": "2025-11"
}
],
"skills": [{ "name": "Product" }, { "name": "AT Protocol" }],
"education": [{ "institution": "…", "degree": "…" }]
}A position with no endedAt is a current role. When a position points at a company page, entityName holds the resolved name. Otherwise company holds the free text that the person typed. The query needs no authentication. It returns the public projection that a logged-out visitor sees, and it omits every hidden item.
The id.sifa.getProfileView lexicon in singi-labs/sifa-lexicons defines the full field list.
Option 2: use the SDK
In a TypeScript app, @singi-labs/sifa-sdk fetches and types the data for you. It also returns a compact summary when you want only the headline facts.
pnpm add @singi-labs/sifa-sdkimport { fetchProfileSummary } from '@singi-labs/sifa-sdk/query'
const summary = await fetchProfileSummary({ baseUrl: 'https://api.sifa.id' }, 'gui.do')
// {
// handle: 'gui.do',
// displayName: 'Guido Jansen',
// headline: 'Founder at Singi Labs',
// currentTitle: 'Founder',
// currentCompany: 'Singi Labs',
// topSkills: ['Product', 'AT Protocol'],
// claimed: true,
// }fetchProfileSummary returns null when there is no profile for that actor, so a missing profile is easy to handle. currentTitle and currentCompany come from the same "primary position" rule the profile page uses, so your summary matches what the person sees on their own profile. Call it from your server, the same as option 1.
If you want the whole profile instead of the summary, use fetchGetProfileView, which returns the full typed ProfileView.
Option 3: read straight from the PDS
The most decentralized path skips Sifa entirely. Every id.sifa.* record lives in the person's own Personal Data Server, and those records are public. Resolve their username to a DID, resolve the DID to its PDS host, then read the collections you want:
# id.sifa.profile.self holds the headline and core fields
curl "https://<their-pds>/xrpc/com.atproto.repo.getRecord?repo=<did>&collection=id.sifa.profile.self&rkey=self"
# positions, skills, and education are one collection each
curl "https://<their-pds>/xrpc/com.atproto.repo.listRecords?repo=<did>&collection=id.sifa.profile.position"This is the same mechanism generic record browsers like pdsls.dev use. The trade-off: you get the raw records exactly as written, so you handle company-page resolution, formatting, and the skill-to-position links yourself. Options 1 and 2 do that work for you. Reach for this one when you specifically want zero dependency on Sifa's AppView. See Apps, AppViews, and the firehose for how PDS reads fit the wider picture.
Validating the records you read
In a TypeScript app, the root subpath of the SDK exports a Zod schema and a type for every id.sifa.* record. They mirror the lexicons in singi-labs/sifa-lexicons, and they pull in no dependencies. Use them for PDS reads. The /query layer and the AppView stay out of it.
import { ProfilePositionRecordSchema } from '@singi-labs/sifa-sdk'
const parsed = ProfilePositionRecordSchema.safeParse(record.value)
if (parsed.success) {
// parsed.data.title, parsed.data.startedAt, parsed.data.skills, ...
}Two things to know about these schemas:
- They accept fields they do not know. A record schema passes an unknown key through, and it strips nothing. A record from a newer lexicon than your pinned SDK still parses. Every field that the SDK does not declare comes back untyped.
- Not every reference carries a CID.
position.skillsentries areid.sifa.defs#skillRef, which is an AT-URI and nothing else: skills are mutable records in the same person's repo, so the link resolves live and follows edits. Other references, such asendorsement.skill, are fullcom.atproto.repo.strongRefvalues that pin one version. Check the lexicon before you assume that acidis there.
If a schema and its lexicon disagree, that is a bug in the SDK. Open an issue and we will fix the schema.
Be a good neighbour
- Cache. A person's role does not change by the second. Cache reads for a minute or so rather than fetching on every page view, and read server-side rather than from every visitor's browser.
- Expect change. A person can edit or delete a record at any time. Treat a
404as "no data right now", not an error, and do not hard-fail a page over a missing profile. - Attribute. When you show Sifa data, link back to the person's Sifa profile page (their
/p/{username}page). That way a visitor has easy access to the full picture and the owner has an easy path to update anything if needed.
Tell us what you built
If your app reads id.sifa.* records, open an issue and we will list it on Lexicons and integrations. Seeing the same profile show up across apps, owned by the person and not by any one platform, is the entire point.