Sub Users

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_users feature. All management requests below use your standard bearer token authentication.

The sub-user object

FieldTypeDescription
iduuidAssigned by SelfDecode. Read-only.
namestring (≤255)Display name for this end-customer.
external_idstring (≤255), optionalYour own identifier for this end-customer. Unique within your account.
contextstring (≤2000), optionalInitial 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_idsuuid[], optionalThe profiles this sub-user may access. Must be profiles owned by (or shared with) your account.
created_at / updated_atdatetimeRead-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-tokenissue-iframe-handoff
What you getA bearer token for the B2B APIA single-use handoff token (OTT)
Who uses itYour backend, or any system calling the API for this end-customerOnly the embedded chatbot page, in the browser
LifetimeUp to 30 days60 seconds, one use
Can call the API?Yes — the whole sub-user surface belowNo. It is exchanged for an HttpOnly chat session cookie and nothing else
Use it forServer-to-server integrations, AI agents, per-customer backendsEmbedding 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/

MethodPathDescription
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"}.

FieldTypeDefaultDescription
expires_ininteger (60–2592000)86400Token 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

StatusCause
400Validation error — e.g. duplicate external_id, expires_in out of range, linking a profile your account cannot access
401Missing or expired bearer token
403Contract 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
404Unknown sub-user id