Requesting account fields from an app
Introduction
Section titled “Introduction”An account holder’s own details, their email address, their language, how much storage they use, are not ordinary events. They are account fields, exposed as system streams, and an application can be granted access to them through the normal authorization flow, with the holder’s explicit consent at the usual consent step.
This guide is for application developers. It covers which fields can be requested, how to spell their stream ids, which permission level to ask for, and how to read the value back. For the operator-side view, how the field schema is configured, see System streams.
The most common case: an application that wants the holder’s email address without asking them to type it a second time.
The two prefixes
Section titled “The two prefixes”Account fields carry one of two prefixes, and which one a field takes is the single most common source of confusion.
| Prefix | Meaning | Examples |
|---|---|---|
:_system: |
built into Pryv.io | :_system:language, :_system:storageUsed |
:system: |
defined by the platform’s configuration | :system:email |
The email is a platform-defined field, so it is :system:email, not :_system:email. It is not built in because some platforms deliberately omit an email address entirely, for account anonymity. The same applies to any additional field an operator declares, such as an insurance number.
There is no :_system:email.
What you get back when you request it depends on the version. Up to and including 2.0.0-rc.17, the request is refused with a bare 403 forbidden carrying no message, which is easy to mistake for a deliberate policy refusal rather than a wrong id. A later release replaces that with a 400 naming the id and restating the prefix rule. If you are on a current deployment and see an unexplained 403 on an account stream, suspect the prefix first.
If you are unsure what a given platform declares, ask its operator: the account-field schema is deployment configuration, and every core in a platform shares it.
What can be requested
Section titled “What can be requested”A field is requestable only if it is visible. Built-in bookkeeping fields (appId, invitationToken, referer) are marked not-shown and are refused with a 400; they are not part of an application’s surface.
Practically, on a default configuration:
| Field | Stream id |
|---|---|
:system:email |
|
| Language | :_system:language |
| Storage used | :_system:storageUsed |
Asking for it
Section titled “Asking for it”Include the permission in requestedPermissions exactly as you would for any stream. A defaultName is required:
{ "requestingAppId": "my-app", "requestedPermissions": [ { "streamId": ":system:email", "level": "read", "defaultName": "Email" } ]}The consent screen resolves the field’s configured label, so the holder sees Read "Email" rather than a raw stream id.
Ask for read. Only the personal access writes account fields: for any other access (app, shared, or granted through a consent or an OAuth2 authorization) a level above read on an account stream is refused (since 2.0.0-rc.65), and its writes there answer 403 forbidden. The email address cannot be set through the events API at all: a write to :system:email is refused whatever the permission level. Changing the primary address goes through the account-update flow, which carries the platform’s uniqueness and verification handling; an application that needs the holder’s address should read it, not try to set it.
A caution on the plain name: requesting email with no prefix is currently accepted, but it is a different, ordinary stream that merely looks right on the consent screen. It will be empty, and it will stay empty. Always include the prefix.
Reading the value
Section titled “Reading the value”Account fields are read through the events API, like any other stream:
GET /events?streams=[":system:email"]The response carries one event for that field, whose content is the current value. Its time and modified are when that value was set, so modifiedSince works on account fields as on any other stream.
Three things to know about what you get:
- For an application, wildcard queries never expand into the account namespace: you must name the stream explicitly. (A personal token’s
*does include the account fields.) - An application granted read on the email sees the current primary address only. An account can hold further addresses, including ones awaiting verification, and none of them is reachable through this permission, even by naming their stream.
- The verification state of that address is available on request, as a second, read-only event in the same stream (next section). A read without
typesreturns the address event only.
Is the email verified?
Section titled “Is the email verified?”Being the primary address does not by itself mean the holder has proved control of that inbox: an address set at registration is asserted, not inbox-verified. Ask for the verification state by type, in the same stream:
GET /events?streams=[":system:email"]&types[]=verification/emailIt returns an event of type verification/email, id :system:emailVerification (also readable with GET /events/:system:emailVerification):
{ "id": ":system:emailVerification", "streamIds": [":system:email"], "type": "verification/email", "content": { "verified": true, "method": "email-link", "verifiedAt": 1759900000 }}verifieddescribes the address currently in theemail/stringevent. It istrueonly when ownership of that address was proved:methodisemail-link(the holder clicked a link mailed to the address),email-code(pasted a code mailed at registration) oroperator(confirmed by the platform operator).registration(the founding address) andlegacy(set without proof) read asverified: false.verifiedAtis the time of the proof, ornull.- When the primary address changes, the event follows the new address: an unproved new primary reads
verified: false. The event is never removed while an address exists. - It is derived by the server at read time: it cannot be written and has no history (socket.io clients get
eventsChangedwhen the address or its proof changes).
To get both at once, ask for both types: types[]=email/string&types[]=verification/email (the address comes first). verification/* works too. Without types, the stream returns the address event only, as it always has.
No extra permission is needed: an access that can read the email can ask for its verification state.
Why not a copy of your own
Section titled “Why not a copy of your own”An application can always ask the holder to type their address again and store it as an ordinary event. That works, and it is what applications did before this was documented, but it creates a second copy of the same personal data with its own lifecycle: it does not follow a change of account email, and clearing it is a separate obligation from deleting the account.
Requesting the field keeps one authoritative copy, which is the better answer for data minimisation and the one to prefer where the platform declares the field.
Related
Section titled “Related”- System streams, the operator-side configuration of the field schema.
- Consent implementation, for the consent flow these permissions travel through.
- Application guidelines.

