Scope API access to a single end-customer with automatic filtering and hard security boundaries.
A sub-user represents one of your end-customers inside your SelfDecode account. Once created, you can mint tokens scoped to that sub-user: any request made with a sub-user token is automatically filtered to the profiles you linked to it, with no query parameters or extra logic on your side.
Use sub-users whenever a system acting for a single end-customer needs to call the SelfDecode API — a per-customer backend service, an AI agent, or the embedded chatbot, which is built on top of sub-users.
Prerequisites
Your contract must include the
sub_usersfeature. All management requests below use your standard bearer token authentication.
The sub-user object
| Field | Type | Description |
|---|---|---|
id | uuid | Assigned by SelfDecode. Read-only. |
name | string (≤255) | Display name for this end-customer. |
external_id | string (≤255), optional | Your own identifier for this end-customer. Unique within your account. |
context | string (≤2000), optional | Initial context injected into AI sessions for this sub-user (see embedded chatbot). This is seed context only — the chatbot maintains its own memory of the user across conversations. HTML is stripped. |
profile_ids | uuid[], optional | The profiles this sub-user may access. Must be profiles owned by (or shared with) your account. |
created_at / updated_at | datetime | Read-only. |
Two ways to use a sub-user
Once a sub-user exists, there are two separate things you can mint from it. They are not steps in one flow — pick the one that matches where the code runs.
issue-token | issue-iframe-handoff | |
|---|---|---|
| What you get | A bearer token for the B2B API | A single-use handoff token (OTT) |
| Who uses it | Your backend, or any system calling the API for this end-customer | Only the embedded chatbot page, in the browser |
| Lifetime | Up to 30 days | 60 seconds, one use |
| Can call the API? | Yes — the whole sub-user surface below | No. It is exchanged for an HttpOnly chat session cookie and nothing else |
| Use it for | Server-to-server integrations, AI agents, per-customer backends | Embedding the AI chatbot |
You never need both: the chatbot embed uses only the handoff, API integrations use only the bearer token. Both resolve to the same sub-user and the same linked profiles.
Managing sub-users
Base URL: https://{env}selfdecode.com/service/b2b-integrations/sub-user/
| Method | Path | Description |
|---|---|---|
| GET | /sub-user/ | List your sub-users |
| POST | /sub-user/ | Create a sub-user |
| GET | /sub-user/{id}/ | Retrieve a sub-user |
| PUT | /sub-user/{id}/ | Update a sub-user (e.g. link more profiles) |
| DELETE | /sub-user/{id}/ | Delete a sub-user — its tokens stop working immediately; the linked profiles and their data are untouched |
| POST | /sub-user/{id}/issue-token/ | Mint a bearer token scoped to this sub-user, for server-to-server API calls |
| POST | /sub-user/{id}/issue-iframe-handoff/ | Mint a 60-second single-use handoff for the chatbot embed — not an API credential |
Create a sub-user
import os
import requests
env_domain = os.getenv("SD_ENV") # "qa-b2b." for testing, "" for prod
base_url = f"https://{env_domain}selfdecode.com/service/b2b-integrations"
headers = {"Authorization": f"Bearer {os.getenv('ACCESS_TOKEN')}"}
response = requests.post(
f"{base_url}/sub-user/",
headers=headers,
json={
"name": "Jane Doe",
"external_id": "customer-4812",
"profile_ids": ["1f0c9067-72b3-4a5e-9d55-2f7a1c8f3b21"],
},
)
sub_user = response.json()
Response:
{
"id": "7c8a1c0e-4b7e-4e2d-b7de-91a3f2b6c554",
"name": "Jane Doe",
"external_id": "customer-4812",
"context": "",
"profile_ids": ["1f0c9067-72b3-4a5e-9d55-2f7a1c8f3b21"],
"created_at": "2026-07-06T09:00:00Z",
"updated_at": "2026-07-06T09:00:00Z"
}
Issuing sub-user tokens
response = requests.post(
f"{base_url}/sub-user/{sub_user['id']}/issue-token/",
headers=headers,
json={"expires_in": 86400},
)
sub_user_token = response.json()["token"]
Response: {"token": "<jwt>", "expires_at": "2026-07-07T09:00:00Z"}.
| Field | Type | Default | Description |
|---|---|---|---|
expires_in | integer (60–2592000) | 86400 | Token lifetime in seconds (maximum 30 days). |
expires_in is the only field. Per-session capability narrowing (mcp_config) is specific to the embedded chatbot handoff — a bearer sub-user token always carries the whole sub-user API surface your contract grants.
Using sub-user tokens
A sub-user token is a drop-in bearer token for the standard B2B API. The difference is scope: every request is automatically filtered to the sub-user's linked profiles.
# Same endpoint you'd call with your account token — but the response
# only contains the profiles linked to this sub-user.
response = requests.get(
f"{base_url}/profile/",
headers={"Authorization": f"Bearer {sub_user_token}"},
)
Available with a sub-user token (always limited to linked profiles):
- Profiles — read-only (list, retrieve). Creating, updating and deleting profiles stays on your account token.
- Reports — report jobs (health, ancestry, PGx, whole genome PGx, rare variants, longevity, carrier status, ACMG, clinical overview, biochemistry, hair loss, custom SNP, health overview), composed report retrieval, per-profile report status and outcomes, and each report's own recommendations
- Genome files — genome file records, upload jobs (including bulk upload) and SNP file jobs
- Labs — results (numeric, discrete, composed), lab OCR jobs, and PDF jobs
- Orders — orders and DNA kit jobs
- Questionnaires — answers, questionnaire statuses, and lifestyle assessment summaries
- Diet & nutrition — composed diets, meal plans, daily meal plans, meals
- Genes — genotypes and profile user collections
Not available with a sub-user token:
- Sub-user management (a sub-user cannot create or modify sub-users)
- Webhooks and account configuration, including intake form definitions
- Account-wide catalog and summary endpoints — including the general recommendation library (
/recommendation/,/recommendation-id/,/recommendation-summary/), which is account-token only. A sub-user reaches recommendations through its profiles' reports instead.
Contract features still apply
Sub-user scoping and feature entitlement are independent gates. Every endpoint above is also gated on the matching contract feature, so a request into a domain your contract does not include returns 403 no matter which token you use. The features are reports, ancestry, pgx, wgs_pgx, wgs_rare_variants, longevity, carrier_status, acmg, clinical_overview, bio_chemistry, hair_loss, custom_snp, health_overview, genome_file, snp_file, genes, labs, orders, lifestyle_assessment, action_plan (i.e. recommendations), plus sub_users and chatbot.
Security model
- Automatic scoping. Profile filtering is enforced server-side on every data path — a sub-user token cannot read or write data for a profile you haven't linked, even by addressing it directly by id.
- Fail-closed. Endpoints not explicitly enabled for sub-user tokens reject them. New API surface is inaccessible to sub-user tokens by default.
- No privilege inheritance. Sub-user tokens never carry administrative or account-management rights, regardless of the account that minted them.
- No destructive profile access. Profile creation, mutation and deletion are account-token operations — a sub-user token cannot delete an end-customer's profile or the reports and lab results under it.
- Isolation between sub-users. Two sub-users of the same account cannot see each other's data or chatbot conversations.
Errors
| Status | Cause |
|---|---|
| 400 | Validation error — e.g. duplicate external_id, expires_in out of range, linking a profile your account cannot access |
| 401 | Missing or expired bearer token |
| 403 | Contract does not include the sub_users feature (or the feature behind the endpoint you called), or a sub-user token was used on an endpoint or action that does not allow it |
| 404 | Unknown sub-user id |
