Skip to main content

Profile

GET /v1/profile reads the profile of the user who granted your app access. It is one of the DFOS API's credential-gated routes — the others are the membership routes and GET /v1/credential — and every other endpoint is anonymous. The credential itself comes from Sign in with DFOS, where the user approves a read:profile and/or read:email grant on a DFOS-hosted consent screen.

curl https://api.dfos.com/v1/profile \
-H 'Authorization: DFOS <request proof>' \
-H 'X-Credential: <credential>'

Under a credential covering read:profile read:email:

{
"did": "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc",
"username": "bvalosek",
"displayName": "Brandon",
"description": "Building things in Austin.",
"avatarUrl": "https://cdn.dfos.com/…",
"email": "brandon@example.com",
"createdAt": "2025-11-04T16:22:00.000Z"
}

Under a credential covering read:profile alone, email is absent — not null:

{
"did": "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc",
"username": "bvalosek",
"displayName": "Brandon",
"description": "Building things in Austin.",
"avatarUrl": "https://cdn.dfos.com/…",
"createdAt": "2025-11-04T16:22:00.000Z"
}

And under a credential covering read:email alone, the profile fields are absent too. This is the whole response:

{
"did": "did:dfos:6encc4akrze2ah9kntzd9tc8zr24crc",
"email": "brandon@example.com"
}

Which scopes open which fields

Either credential scope opens the route, and then each scope serves its own fields. Only did is unconditional:

Your credential carriesRouteProfile fieldsemail
read:profile200presentabsent
read:email200absentpresent
both200presentpresent
neither403

"Profile fields" means username, displayName, description, avatarUrl, and createdAt.

read:email stands on its own: an app that only needs an account key can ask for it alone, call this route, and get back exactly {did, email}. It is not given a name and a handle it did not ask for — a DFOS profile can be private, in which case those fields are served nowhere anonymously either, so "they're public anyway" is not a reason to include them.

The scope decides the shape, and nothing else does. A user's privacy toggle never changes what your credential returns: a subject reading their own data through a grant they issued is always permitted, so the same credential gets the same fields whether or not their profile is publicly listed.

There is no path parameter

You cannot name a user. The route has no {user} segment, and adding one to the URL does not address anyone else's profile — the credential you present selects the subject, and the subject is always the person who issued that credential.

This is the design, not an omission. An endpoint with a user parameter would have to answer "may I read this person's profile?" on every request, and would leak whether a named user exists at all. With the subject fixed by the credential, there is no identifier for a caller to probe.

Fields

FieldServed underNotes
didalwaysThe user's protocol DID. Canonical and stable — store this.
usernameread:profileTheir handle, or null if unset. A mutable alias; never key on it.
displayNameread:profileDisplay name, or null.
descriptionread:profileProfile bio, or null.
avatarUrlread:profilePermanent public CDN URL for their avatar, or null if they have none.
createdAtread:profileWhen they joined DFOS (ISO 8601 UTC).
emailread:emailThe account email — available nowhere else on this API.

did is unconditional because it is not a disclosure: it is the identity your credential is already rooted at, so naming it back tells you nothing you did not arrive holding.

Every other field is absent, never null, when your grant does not cover it. null on this response means "the user has none" — an unset handle, an empty bio — so absence is an authorization fact, not a data fact. Check for the key, not for a falsy value.

avatarUrl is a permanent, unsigned CDN link — the same one GET /v1/users/{user} serves — because an avatar is public media. It is stable while the media is referenced, so it is safe to put in an <img> and cache alongside the display name. It is not a signed URL and carries no expiry. No other media appears on this response.

Calling it

Acting for a user, the request carries two headers — the credential and a fresh request proof signed over this exact method, host, path, and (empty) body. The mechanics, the freshness window, and the refusal codes are all in Authentication; the credential lifecycle is in Scopes and credentials.

Reading your OWN profile needs no credential: sign an identity proof with one of your own keys and send Authorization alone, per Reading your own data needs no credential. That call returns every field above, because there is no narrower grant to apply.

How to get a credential to call this with: Set up § 7 takes you from the scope set you ask for to a runnable call against this route.

In short:

  • 401 — your proof was wrong. Sign a fresh one and retry.
  • 403 — your proof was right and your grant is not. The credential was revoked, expired, or carries neither read:profile nor read:email for this host. Retrying will not help; ask the user for a new grant. Note that a credential missing only ONE of the two scopes is not a 403 — the route succeeds and the fields that scope covers are simply absent.
  • 503 — DFOS could not complete verification. Retry with backoff.

A revoked credential stops working on the very next request. Handle 403 as a normal, expected state — it is how a user withdrawing access reaches your app — rather than as an error to retry through.

Caching and privacy

This response is served Cache-Control: no-store and is never shared-cached: unlike every anonymous response on this API, it is specific to one viewer. Don't put it behind a shared cache or CDN of your own either.

The email you read — when your grant covers read:email — is the user's real account email. Revoking the credential ends your access immediately; it does not un-share what you already read, so store only what your app actually needs, and delete it when the grant ends. Asking for read:email only when you actually need it is the cheapest privacy decision available to you, and users approve shorter lists more often.