Account delegation
Account delegation lets one Pryv.io account hold full control over another. It is the mechanism for parent- or guardian-controlled accounts: a parent manages a young child’s account, or a caregiver manages the account of a dependent adult, using their own everyday login.
It complements the Cross-account Messaging & Consent (CMC) guide, but the two solve different problems. CMC shares selected, permission-bounded data between two independent account owners, each of whom stays in control of their own account. Delegation grants full, owner-equivalent control of one account to another. Reach for delegation when one person is responsible for another person’s account, not merely sharing data with them.
Vocabulary
Section titled “Vocabulary”A delegation relationship is a directed link between exactly two accounts:
- The controlled account is the account being managed. This is the child, or the dependent adult.
- The delegate is the account holding control. This is the parent, or the caregiver.
The same account can sit on both sides of different relationships: a parent’s account is a delegate of their child’s account, while another caregiver may in turn be a delegate of the parent’s own account.
Two phrasings describe a relationship from each side:
| Phrasing | Point of view | Means |
|---|---|---|
| “the accounts I manage” / “my controlled accounts” | the delegate | the accounts this account controls |
| “my delegates” / “the accounts my account is delegated to” | the controlled account | the accounts that control this one |
A relationship has one of two states:
invite— the controlled account has requested a delegate, and the delegate has not yet accepted.active— the delegate has accepted, or the account was created directly by a delegate (active immediately).
There is no resting “detached” state: removing a delegate deletes the relationship. The full history lives in the audit trail.
An account can have several delegates at once (for example both parents of a child). Each relationship is fully independent: removing one delegate leaves the others untouched.
The trust model — what a delegate can do
Section titled “The trust model — what a delegate can do”A delegate holds an owner-equivalent token over the controlled account. There is exactly one thing it cannot do: remove a delegation relationship. Everything else an account owner can do, a delegate can do.
A delegate has full control of the controlled account. A delegate can read and change all of its data; manage its streams and accesses; change its password, email and MFA; delete the account entirely; and add further delegates. Because a delegate can set the account’s password, a delegate can also log in to the account directly, which includes the power to remove other delegates. Removing any delegate requires logging in to the controlled account itself. Every credential change, login, and removal is recorded in the account’s audit trail.
This model is deliberate. The primary use case is a trusted parent or guardian who is responsible for an account they created. Two consequences follow and should be surfaced to users at the moment they grant delegation:
- A delegate who holds the account’s credentials controls the account outright. A malicious delegate can change the password and MFA, locking the genuine owner out, and can log in as the account to remove other delegates. Delegation is a full trust relationship, not a limited grant.
- The adult-to-adult case deserves the loudest warning. A dependent adult delegating to a caregiver is trusting that caregiver at password-custody level. That includes the power to log in as them and to remove other delegates.
The protection that always holds is attribution: every action a delegate performs on the controlled account, including credential changes, added delegates, token issuance, and deletions, is recorded in the controlled account’s audit trail and attributed to that specific delegate (see Auditing).
There is no scoped or read-only delegate in this version. If you need permission-bounded sharing rather than full control, use CMC instead.
Creating a controlled account from a delegate
Section titled “Creating a controlled account from a delegate”A delegate can create a brand-new controlled account in one call. The relationship is active immediately, with no handshake and no consent step, because the account is created by, and for, the delegate.
const res = await parentConn.api([{ method: 'delegations.createAccount', params: { username: 'kim-doe', email: null, // optional password: null, // optional core: null, // optional target core on a multi-core platform language: 'en'}}]);// res[0].delegation = { relId, controlled: { username, hostSlug }, status: 'active', activatedAt }Both email and password are optional:
- Without an email, the account simply has no email on file.
- Without a password, the account is created with an unguessable random password and cannot be logged into directly. It is reachable only through its delegates.
An account with no usable password is exactly what a parent wants for a young child: there is no login for the child to lose or leak, and the parent operates it entirely through delegation. The way out of that state is the majority handover, where a delegate sets a real password (or an email plus a password reset), the account owner logs in for the first time, and can then take over.
On a multi-core platform, core selects which core hosts the new account (it defaults to the delegate’s own core). On a single-core platform, omit it.
Requesting a delegate — the attach handshake
Section titled “Requesting a delegate — the attach handshake”For an account that already exists and has its own credentials (the dependent-adult case), delegation is always initiated by the controlled account. There is no way for one account to unilaterally take control of another: the account being controlled must ask first, and the delegate must accept.
1. The controlled account requests a delegate.
const res = await principalConn.api([{ method: 'delegations.requestAttach', params: { delegateUsername: 'carla-care'}}]);// res[0].delegation = { relId, delegate: { username }, status: 'invite', requestedAt, expiresAt }This creates a pending invite. On a multi-core platform, the request is delivered to the delegate’s core automatically. The invite carries an expiry (30 days by default).
2. The delegate accepts (or refuses).
await caregiverConn.api([{ method: 'delegations.acceptAttach', params: { username: 'principal-pat' // the controlled account's username}}]);// -> relationship becomes 'active'or
await caregiverConn.api([{ method: 'delegations.refuseAttach', params: { username: 'principal-pat'}}]);Refusing is the delegate’s unilateral decline of a pending invite; it is not a detach and is always available to the delegate.
3. Either side can list its relationships.
// The controlled account sees who controls it:await principalConn.api([{ method: 'delegations.listDelegates' }]);// { delegates: [ { relId, delegate: { username, hostSlug }, status, requestedAt, activatedAt, lastTokenIssuedAt } ] }
// The delegate sees the accounts it manages:await caregiverConn.api([{ method: 'delegations.listControlled' }]);// { controlled: [ { relId, controlled: { username, hostSlug }, status, requestedAt, activatedAt } ] }A pending invite can be withdrawn by the controlled account with delegations.cancelInvite (this is subject to the same genuine-login rule as detach, below).
Acting as a controlled account
Section titled “Acting as a controlled account”To operate a controlled account, a delegate mints a delegate token on demand:
const res = await parentConn.api([{ method: 'delegations.getToken', params: { username: 'kim-doe'}}]);const { token, apiEndpoint } = res[0];
// The delegate now talks to the controlled account's core directly:const kidConn = new pryv.Connection(apiEndpoint /* with token */);await kidConn.api([{ method: 'events.get', params: { limit: 20 } }]);The delegate token is a personal-class token: it can do everything the account owner could do. It is minted freshly each time and follows the same lifetime as a normal login session; calling getToken again re-issues it. The control channel that authorizes minting never leaves the delegate’s core; only the delegate token and the controlled account’s endpoint are returned to the client.
When a delegate token calls access-info, the result carries an additive delegation field so the client knows it is operating a delegated account (see Recognising a delegated token).
Removing a delegate — the genuine-login rule
Section titled “Removing a delegate — the genuine-login rule”Every power a delegation grants lives in tokens stored on the controlled account. Removing them there is the authoritative teardown, and it is the controlled account’s exclusive right.
Removing a delegate requires a genuine login on the controlled account. The request must be authenticated by a token that came from the controlled account’s own login flow (its password, plus MFA if configured). A delegate token cannot remove a delegation, not another delegate’s and not its own.
// Authenticated as a genuine login on the controlled account:await controlledConn.api([{ method: 'delegations.detachDelegate', params: { username: 'carla-care'}}]);Attempting this with a delegate token is rejected with delegation-genuine-login-required (403).
A detach immediately and permanently destroys the delegate’s token and control channel on the controlled account, so the delegate loses access on its very next request, regardless of network reachability between cores. Delegation resources cannot be deleted through the generic accesses.* or events.* APIs by any token, so this rule cannot be side-stepped.
There is deliberately no delegate-initiated detach in this version: a delegate cannot walk away on its own. A parent who created a passwordless child account must not be able to abandon it and strand it with no way in. The way a delegate is released is always through the controlled account logging in genuinely, as in the handover below.
The exact boundary. The rule proves the token came from the controlled account’s login flow; it does not prove who performed the login. A delegate that holds the account’s credentials can set its password, log in as the account, obtain a genuine-login token, and use it to remove other delegates. This is a direct consequence of the owner-equivalent trust model, not a gap. Each such step (the password change, the login, the removal) is recorded and attributed in the audit trail.
Worked walkthroughs
Section titled “Worked walkthroughs”Parent and child
Section titled “Parent and child”- Create. A parent creates the child’s account from their own account with
delegations.createAccount(no email, no password). It isactiveimmediately. - Operate. The parent calls
delegations.getTokenwhenever they need to act as the child, and manages the child’s data with the returned token. - Add the other parent. From a delegate token acting as the child, the parent calls
delegations.requestAttachnaming the other parent, who accepts withdelegations.acceptAttach. The child now has two independent delegates.
Dependent adult and caregiver
Section titled “Dependent adult and caregiver”- Request. The dependent adult, logged into their own account, calls
delegations.requestAttachnaming the caregiver. - Accept. The caregiver accepts with
delegations.acceptAttach. Before accepting, they should be shown the trust-model warning: accepting means holding full, password-level control of the other person’s account. - Operate. The caregiver uses
delegations.getTokento act on the dependent adult’s account. - Release. To remove the caregiver, the dependent adult logs into their own account and calls
delegations.detachDelegate. A caregiver cannot remove itself.
Majority handover
Section titled “Majority handover”When a child reaches an age where they should own their account, or any owner takes over a delegate-run account, no new mechanism is needed:
- Using a delegate token, the parent either sets a password on the account directly and hands it over, or sets the account’s email and triggers the standard password reset so the new owner receives it.
- The new owner logs in genuinely with their own credentials, obtaining a real login token that carries no delegation marker.
- Only now, from that genuine login, can they call
delegations.detachDelegateand remove the parent (and any other delegate).
The genuine-login detach rule and the delegate’s power to set credentials fit together to produce a clean handover, using nothing more than the password-reset flow that already exists.
Cross-core, same platform
Section titled “Cross-core, same platform”On a multi-core Pryv.io platform, the delegate and the controlled account can live on different cores of the same platform. The delegation methods handle the core-to-core delivery for you: creating an account on another core, delivering an attach invite to the delegate’s core, minting a delegate token across cores, and propagating a detach. The account tokens themselves never move between cores; each core issues and holds its own.
Cross-platform delegation (accounts on two different Pryv.io platforms) is not supported. That is CMC’s territory.
Recognising a delegated token
Section titled “Recognising a delegated token”Delegation surfaces additively on access-info; nothing about existing responses changes. When the calling token is a delegate token, the result gains:
{ "delegation": { "isDelegatedAccess": true, "controlledUsername": "kim-doe", "delegate": { "username": "parent-doe", "hostSlug": "example-core" } }}The token acts as the controlled account, so access-info’s user.username remains the controlled account. A client can read the delegation field to know it is operating on a delegated account, and to display which delegate is acting.
Auditing
Section titled “Auditing”Delegation is fully attributable:
- Per-delegate attribution is automatic. Each delegate holds its own distinct token on the controlled account, so every action lands under that token in the account’s audit trail. A delegate’s activity is never blended with the owner’s or with another delegate’s.
- Delegate identity on the record. Audit events produced by a delegate token carry an additive
content.delegation = { delegateUsername, delegateHostSlug }, so the acting delegate is legible directly on the record. - Lifecycle events are audited. Requests, accepts, refusals, token issuances, account creation, and detaches each leave an audit record on the account they execute on.
API reference
Section titled “API reference”All methods below belong to the delegations.* family, under the /delegations path on a user’s API endpoint. Controlled account methods run on the account being managed; delegate methods run on the controlling account. Error ids are namespaced delegation-*.
Two token requirements appear:
- Personal token — a standard personal (login) token. A delegate token counts as personal for every method except detach and invite-cancel.
- Genuine login — a personal token that came from the account’s own login flow and carries no delegation marker. A delegate token is rejected.
The whole family can be switched off by an operator via the delegation:active configuration flag (enabled by default); when disabled, none of these methods are registered.
Request a delegate
Section titled “Request a delegate”| id | delegations.requestAttach |
| HTTP | POST /delegations/attach-request |
| side | controlled account |
| token | personal (delegate tokens included) |
Requests attachment of a delegate. Creates a pending invite relationship and delivers it to the delegate’s core.
Parameters
delegateUsername |
string — the account to request as a delegate. |
Result HTTP 201 Created
{ "delegation": { "relId": "…", "delegate": { "username": "carla-care" }, "status": "invite", "requestedAt": 1789000000, "expiresAt": 1791592000 } }Errors
| Status | Error id | |
|---|---|---|
| 404 | delegation-unknown-username |
The delegate username does not resolve to an account. |
| 400 | delegation-self-not-allowed |
An account cannot delegate to itself. |
| 409 | delegation-already-exists |
A pending or active relationship to this delegate already exists. |
| 503 | delegation-delivery-failed |
The delegate’s core could not be reached; the request leaves no trace. |
| 403 | forbidden |
A non-personal (app or shared) token was used; a personal token is required. |
Accept a delegate invite
Section titled “Accept a delegate invite”| id | delegations.acceptAttach |
| HTTP | POST /delegations/controlled/{username}/accept |
| side | delegate |
| token | personal (delegate tokens included) |
Accepts a pending invite, activating the relationship. {username} is the controlled account.
Result HTTP 200 OK
{ "delegation": { "relId": "…", "controlled": { "username": "principal-pat", "hostSlug": "example-core" }, "status": "active", "activatedAt": 1789000100 } }Errors
| Status | Error id | |
|---|---|---|
| 404 | delegation-not-found |
No pending invite from this account. |
| 410 | delegation-invite-expired |
The invite has expired. |
| 400 | delegation-delegate-mismatch |
The invite was not addressed to this account. |
| 503 | delegation-delivery-failed |
The controlled account’s core could not be reached. |
Refuse a delegate invite
Section titled “Refuse a delegate invite”| id | delegations.refuseAttach |
| HTTP | POST /delegations/controlled/{username}/refuse |
| side | delegate |
| token | personal (delegate tokens included) |
Declines a pending invite. This is a handshake decline, not a detach, and is always available to the delegate. Result: HTTP 200 OK, empty body.
Cancel a pending invite
Section titled “Cancel a pending invite”| id | delegations.cancelInvite |
| HTTP | POST /delegations/delegates/{username}/cancel |
| side | controlled account |
| token | genuine login |
Withdraws a pending invite the controlled account sent. Because it removes a relationship record, it requires a genuine login (a delegate token cannot cancel invites). Result: HTTP 200 OK, empty body.
Errors
| Status | Error id | |
|---|---|---|
| 403 | delegation-genuine-login-required |
Called with a delegated (or non-personal) token. |
| 404 | delegation-not-found |
No pending invite for this delegate. |
List my delegates
Section titled “List my delegates”| id | delegations.listDelegates |
| HTTP | GET /delegations/delegates |
| side | controlled account |
| token | personal (delegate tokens included) |
Lists the accounts that control this account. Token values, control endpoints, and capability URLs are never included.
Result HTTP 200 OK
{ "delegates": [ { "relId": "…", "delegate": { "username": "carla-care", "hostSlug": "example-core" }, "status": "active", "requestedAt": 1789000000, "activatedAt": 1789000100, "lastTokenIssuedAt": 1789000500 } ] }List accounts I manage
Section titled “List accounts I manage”| id | delegations.listControlled |
| HTTP | GET /delegations/controlled |
| side | delegate |
| token | personal (delegate tokens included) |
Lists the accounts this account controls. A row’s status may be invite, active, or stale (a local marker meaning the relationship is no longer reachable on the controlled account; see Dismiss).
Result HTTP 200 OK
{ "controlled": [ { "relId": "…", "controlled": { "username": "kim-doe", "hostSlug": "example-core" }, "status": "active", "requestedAt": 1789000000, "activatedAt": 1789000100 } ] }Get a delegate token
Section titled “Get a delegate token”| id | delegations.getToken |
| HTTP | POST /delegations/controlled/{username}/token |
| side | delegate |
| token | personal (delegate tokens included) |
Mints a fresh delegate token for an active controlled account and returns it with the controlled account’s API endpoint. The control channel stays server-side; only the token and endpoint reach the client.
Result HTTP 200 OK
{ "token": "…", "apiEndpoint": "https://kim-doe.example.com/" }Errors
| Status | Error id | |
|---|---|---|
| 404 | delegation-not-found |
No relationship with this account. |
| 410 | delegation-not-active |
The relationship is no longer active on the controlled account (the local row is marked stale). |
| 503 | delegation-delivery-failed |
The controlled account’s core could not be reached. |
Create a controlled account
Section titled “Create a controlled account”| id | delegations.createAccount |
| HTTP | POST /delegations/controlled |
| side | delegate |
| token | personal (delegate tokens included) |
Creates a brand-new account controlled by the caller. The relationship is active immediately.
Parameters
username |
string — the new account’s username. |
email |
string (optional) — omit for an account with no email. |
password |
string (optional) — omit for an account reachable only through its delegates. |
core |
string (optional) — target core on a multi-core platform; defaults to the caller’s own core. |
language |
string (optional) — the account’s language. |
Result HTTP 201 Created
{ "delegation": { "relId": "…", "controlled": { "username": "kim-doe", "hostSlug": "example-core" }, "status": "active", "activatedAt": 1789000100 } }Errors
| Status | Error id | |
|---|---|---|
| 409 | delegation-username-taken |
The requested username is already in use. |
| 400 | delegation-unknown-core |
The requested target core is unknown to this platform. |
| 400 | delegation-self-not-allowed |
The username matches the caller. |
| 502 | delegation-creation-failed |
Account creation failed; any partial state is rolled back. |
Remove a delegate
Section titled “Remove a delegate”| id | delegations.detachDelegate |
| HTTP | DELETE /delegations/delegates/{username} |
| side | controlled account |
| token | genuine login |
Removes a delegate, authoritatively and immediately, on the controlled account. Destroys the delegate’s token and control channel so it loses access on its next request. For a pending invite, this cancels it. Requires a genuine login. Result: HTTP 200 OK, empty body.
Errors
| Status | Error id | |
|---|---|---|
| 403 | delegation-genuine-login-required |
Called with a delegated (or non-personal) token. |
| 404 | delegation-not-found |
No relationship with this delegate. |
Dismiss a stale managed account
Section titled “Dismiss a stale managed account”| id | delegations.dismissControlled |
| HTTP | DELETE /delegations/controlled/{username} |
| side | delegate |
| token | personal (delegate tokens included) |
Removes a stale row from the delegate’s local list. This is housekeeping only: it removes no authority and never touches the controlled account. It is not a detach. Only a stale row can be dismissed. Result: HTTP 200 OK, empty body.
Errors
| Status | Error id | |
|---|---|---|
| 400 | delegation-mirror-not-stale |
The row is not stale; only stale rows are dismissable. |
| 404 | delegation-not-found |
No such row. |
access-info additions
Section titled “access-info additions”access-info gains an additive delegation field when the calling token is a delegate token or a control token. For a delegate token:
{ "delegation": { "isDelegatedAccess": true, "controlledUsername": "kim-doe", "delegate": { "username": "parent-doe", "hostSlug": "example-core" } } }The field is purely additive; existing access-info behaviour is unchanged, and user.username remains the controlled account.
Audit additions
Section titled “Audit additions”Audit events produced by a delegate token (or a control token) carry an additive content.delegation:
{ "content": { "delegation": { "delegateUsername": "parent-doe", "delegateHostSlug": "example-core" } } }Per-delegate attribution is automatic because each delegate holds its own distinct token on the controlled account.
Core-to-core methods
Section titled “Core-to-core methods”The remaining delegations.* methods (issueToken, acceptResponse, refuseResponse, acceptComplete, notifyDetach, under /delegations/controlled-side/*) are used between cores to carry out the handshake, token minting, and detach propagation. They authorize on internal, forge-protected delegation credentials rather than on a client token, and are not part of the client-facing surface. Applications never call them directly; the methods above orchestrate them for you.
Further reading
Section titled “Further reading”- Cross-account Messaging & Consent (CMC) — permission-bounded data sharing between two independent account owners.
- Consent implementation — the classical single-account consent flow.
- Audit logs — how account activity is recorded.
- API reference — the complete method reference.

