Skip to main content

Profile

GET /v1/profile reads the profile of the user who granted your app access — one of this API's credential-gated routes, alongside the membership routes and GET /v1/credential. The credential comes from Sign in with DFOS, where the user approves a read:profile and/or read:email grant.

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:z8zt7ecn9h8n782kae3k796crva2c73",
"username": "bvalosek",
"displayName": "Brandon",
"description": "cofounder / architect @ DFOS and Metalabel . building what i want to see more of ",
"avatarUrl": "https://dfos.imgix.net/media/public/ekceek9z64vz4cavrzr7v2-lil-robo-pfp.png",
"email": "bvalosek@example.com",
"createdAt": "2025-11-19T21:07:44.512Z"
}

Which scopes open which fields​

Either scope opens the route, and each serves its own fields. Only did is unconditional. "Profile fields" means username, displayName, description, avatarUrl, and createdAt.

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

Under read:email alone the whole response is {"did": "did:dfos:z8zt7ecn9h8n782kae3k796crva2c73", "email": "bvalosek@example.com"}. The scope decides the shape and nothing else does: a user's privacy toggle never changes what your credential returns. There is no path parameter — the credential selects the subject.

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.

A field your grant does not cover is absent, never null. null means the user has none, so check for the key, not for a falsy value.

avatarUrl is an unsigned, non-expiring CDN link — the same one GET /v1/users/{user} serves. No other media appears here.

Calling it​

The request carries the credential and a fresh request proof signed over this exact method, host, path, and (empty) body — see Authentication, Scopes and credentials, and Set up § 7.

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. An identity proof presented together with X-Credential is refused 401 with "an identity proof cannot be presented alongside X-Credential"; send one or the other.

StatusMeaning
401Your proof was wrong. Sign a fresh one and retry.
403The credential was revoked, expired, or carries neither scope for this host. Ask the user for a new grant; retrying will not help. Revocation takes effect on the next request, so this is how a user withdrawing access reaches your app. Missing only ONE of the two scopes is not a 403 — those fields are just absent.
413The request body exceeded the maximum this endpoint will authenticate.
429The per-grant budget is exhausted; the body names the scope and a retryAfterMs. See Rate limits and caching.
503DFOS could not complete verification. Retry with backoff.

Caching and privacy​

Cache-Control: no-store, never shared-cached; don't put this response behind a shared cache or CDN of your own either. Revoking ends your access to the account email but not your copy of it, so store only what your app needs, delete it when the grant ends, and ask for read:email only when you need it.