Skip to content

Integration guide

This guide is for integrating an external system, such as a device backend, an EDC or a study-management application, that submits recordings to the youSleep platform and consumes the results programmatically. It covers the whole path and states the limits to design around.

If you are analysing your own files, read the SDK guide instead. This page assumes a server calling on behalf of many subjects.

Before you write code

Five requirements gate access, all of them administrative rather than technical.

Requirement How it is satisfied
An approved account Registration is request-access: a new account cannot log in until an administrator approves it
Permission to create API keys Granted per account by an administrator. Machine access is a partner's need rather than every account's, so it is switched on for the accounts that integrate
Accepted licence and terms Every authenticated route requires current consent. A person is asked on their next sign-in; a key keeps working for a grace period after a new document version, then fails the same way on every endpoint
A usage type that matches your use Assigned at registration, not self-service; it determines which licence you are gated onto
Credits Research analyses are paid from the organisation's research grants, commercial ones from purchased credits. Both are reserved at submission and settled on completion. Confirm the balance, or ask for a grant, before designing a schedule around it

Confirm all five with your youSleep contact before integration work starts. They are the most common reason a correct client receives 401 or 403 on its first call.

Base URL and versions

https://api.yousleep.ai

The SDK compares its own yousleep_common version against the API's on the first request and raises ConnectionError if they are incompatible. This is deliberate; see version pairing. An integration that ships on its own release cadence needs a policy for when it takes an SDK upgrade, because the API is released independently of your backend.

strict_version_check=False downgrades the mismatch to a warning. Use it only as a short-term bridge, not to make a failing build pass.

Authentication

A machine authenticates with an API key: an opaque bearer credential that belongs to one user account and acts as that account, with a permission set narrowed to what the key was scoped to.

from yousleep_common.client import AsyncClient

client = AsyncClient.from_api_key(base_url="https://api.yousleep.ai", api_key="ysk_…")

A key is created on the account page of the portal by a signed-in user whom an administrator has enabled for it. Creation asks for the account password again, names the key, picks its scopes, and sets its lifetime. The full key is shown exactly once; the platform stores only a hash of it and the first characters, so a lost key cannot be recovered, only replaced.

The key design has the following properties:

  • Least privilege. Scope a key to the permissions the integration needs, for example recording:create, analysis:create and analysis:read. The server reports which permissions may be delegated at all (GET /status/permissions, key_scopeable) and refuses any other; account and key management are never among them, so a leaked key cannot change the password, accept legal documents, buy credits or mint further keys. A key also never holds more than its owner holds: withdrawing a permission from the account narrows every key at once.
  • Every key expires. Choose a lifetime when creating it; the server caps it and applies a default when none is given. Rotate by creating the next key before the current one lapses, switching the integration over, then deleting the old key. The owner is emailed seven days before a key expires.
  • Revocation is immediate. Deleting a key, disabling the account, or an administrator's revocation stops it on the next request.
  • Failures are deliberately uniform. An unknown, revoked or expired key answers 401 with the same body; the account page shows a key's state. A 403 means the key authenticated but lacks the scope, or the account's consent has lapsed.
  • Consent has a grace period for machines. When a new version of the terms or a licence is published, a person is asked to accept it on their next sign-in, while requests made with a key keep working for a grace period and carry an X-Consent-Deadline response header naming when they stop. Log that header; the owner is also emailed a week before the deadline.
  • Rate limits are per key. A busy pipeline does not lock its owner out of the portal, and two integrations do not share a window.
  • Attribution. Content the key creates belongs to the owning account like anything else; the audit trail records which key acted.

Hold the key in a secret manager on your backend, never in a browser, a mobile app or device firmware: a key identifies the account it belongs to, and the expected topology is that recordings reach your cloud first and your backend calls the platform.

A person exploring the API interactively signs in with an email and password instead; AsyncClient.from_credentials(...) handles the token refresh. Those sessions are short-lived by design and are the wrong credential for an unattended process.

The resource model

Project ─┬─ Study ── Recording ── Analysis ── Events / Biomarkers
         └─ Study ── Recording ── Analysis ── …

Items are addressed by their own UUID: a recording by its recording id alone, an analysis by its analysis id. Only collections take their parent: client.recordings.list(study_id), client.analyses.list(recording_id). Ids are UUIDs; a name in an id position is a 422, not a lookup. client.recordings.list_for_project(project_id) pages through a project's recordings across all of its studies, and each recording carries its study's id and name.

Two properties of the model affect how an integration is designed:

  • A study holds one recording. One file is one study.
  • A subject is identified by a key rather than a record. Studies of one person share a subject_key (compared case- and whitespace-insensitively) and optionally a session_index. Per-subject figures, series and erasure use these fields. Set both when creating studies, or afterwards in bulk; see study metadata in bulk.

Studies also accept age, sex, bmi and tags. Tags are the grouping axis for project-level reports, so a study tagged with its arm or cohort can be compared against another without further modelling.

flowchart LR
    D[Recorder or device] --> Y[Your backend]
    Y -- upload --> P[youSleep API]
    P --> A[analyses]
    A -- events, statistics --> Y

The expected topology: recordings reach your own systems first, and your backend holds the key and talks to the platform. Nothing on a device or in a browser should hold a key.

Getting data in

client.recordings.upload(study_id, Path("night.edf")) pre-registers the recording with its EDF header, streams the file to a presigned PUT in 64 KiB chunks, and finalises. The bytes go from the machine making the call straight to storage. A recording held in your own object store is fetched by your backend and uploaded the same way.

The PUT sends the headers in presigned_upload.headers, which are signed into the URL. If-None-Match: * among them lets the URL create the object once: a second PUT with it is refused with 412, so nothing can replace a recording after it is checked. A client that uploads without this SDK sends them too, and treats a 412 on a retry as the object already stored.

The upload follows its project's pseudonymisation policy, which is set when the project is created (ProjectCreate.pseudonymisation):

  • The EDF patient and recording identification fields are blanked in every project. If the project removes dates (dates="removed"), the start date becomes EDF+'s unknown date; the time of day stays. Only the uploaded copy is rewritten: your file is not changed, and nothing but the first 256 bytes differs.
  • In a project that codes file names (file_names="coded"), pass name="S001", the code your own records map to the file. youSleep keeps no map. Any project accepts name; the file's extension is added when the name has none.

A client that sends recordings without this SDK applies the rule itself: yousleep_common.edf.pseudonymise_header is the reference, documented in EDF headers.

Submitting and collecting results

from yousleep_common.models import AnalysisRequest

analysis = await client.analyses.submit(
    recording.id, AnalysisRequest(analysis_config_id="u-sleep-research-v2")
)
analysis = await client.analyses.wait_for(analysis.id, timeout=3600)
events = await client.analyses.get_events(analysis.id)
biomarkers = await client.analyses.get_biomarkers(analysis.id)

client.analyses.list_configs() returns the analyses available to you, each with its input requirements (accepted format, channel types, minimum duration), its price per hour of signal (credits_per_signal_hour) and the balance that pays for it (spend_class). Validate recordings against the config you intend to run rather than discovering a mismatch at submission. site_allowance says where your organisation may run it: place is where your organisation's analyses run, and runs_there whether this one may run there. An analysis that runs on the youSleep cloud only does not run at your organisation's own site unless youSleep has recorded an exception for your organisation, which by_exception then says. client.analyses.list_publications() returns the publications behind them, grouped by analysis family.

client.analyses.quote(recording.id, request) takes the same request as submit and returns what the analysis would cost, the balance it would be drawn from, and blocked_reason when the submission would be refused. It creates nothing and reserves no credits. A request that submission would reject fails the quote in the same way, so a quote also checks a request before it is sent.

wait_for raises ClientTimeoutError, not the builtin TimeoutError.

Reading events at scale

get_events returns a flat list[Event], which is the convenient form for one analysis you intend to iterate. It is the wrong form for a pipeline: a full night of 1 Hz sleep staging is tens of thousands of events, and that is per analysis, per night, per subject.

There are two ways to avoid fetching that repeatedly.

Fetch blocks instead. Blocks encode the same events as runs of consecutive equal-duration slots with per-slot attributes as parallel arrays. A night is a handful of blocks rather than tens of thousands of objects.

from yousleep_common.utils.event_blocks import blocks_to_events

document = await client.analyses.get_event_blocks(analysis.id)
document.event_count            # without materialising anything
events = blocks_to_events(document.blocks)   # only if you need them flat

The document is returned whole, without server-side filtering, so fetch it once and slice locally. The block encoding is lossless for any event list: irregular, overlapping or single events become one-slot blocks.

Across many analyses, client.batch.get_event_blocks fetches a document each in one request, up to the server's per-request cap. It carries no filter either, for the same reason.

from yousleep_common.models import BatchGetEventBlocksRequest

response = await client.batch.get_event_blocks(
    project.id, BatchGetEventBlocksRequest(analysis_ids=[a.id, b.id])
)
for entry in response.results.values():
    if entry.status == "ok" and entry.document is not None:
        events = blocks_to_events(entry.document.blocks)

Or filter and page the flat endpoint. get_events narrows on the server. It takes a time window in milliseconds from the recording start, a label or an analysis type to keep or remove, and offset and limit, which are applied after the filters. Without a limit, the client reads every page and returns all the matching events.

from yousleep_common.types import AnalysisType

first_hour = await client.analyses.get_events(
    analysis.id, window_end_time_ms=3_600_000, analysis_type=AnalysisType.STAGING
)

Sporadic events such as arousals and limb movements are sparse enough that the flat form is adequate. Dense per-epoch output is where blocks matter.

Results are presentation-complete: the API computes the statistics, the formatting, the table definitions and the chart geometry, so two renderers of the same payload draw the same thing. Take the values as given rather than recomputing them.

Working at volume

Use the batch endpoints rather than looping. They accept up to 500 items per request and return 207 Multi-Status with a per-item result, so one bad recording does not abort the batch.

Operation Method
Create studies client.batch.create_studies
Price before submitting client.batch.quote_analyses (one recording: client.analyses.quote)
Submit analyses client.batch.submit_analyses
Poll many analyses client.batch.get_analyses_status
Fetch events client.batch.get_events — flat list[Event] per analysis
Fetch event blocks client.batch.get_event_blocks — one block document per analysis; prefer this for dense output, see Reading events at scale
Write subject metadata client.batch.patch_studies_metadata

Two behaviours of the batch endpoints affect how a client is written:

  • A 207 does not mean success. Read every item's status; the summary object carries the counts when you only need to know whether to look closer.
  • Submission is accepted item by item rather than all-or-nothing. Accepted items start running while later ones are still being processed, and a client interrupted mid-batch keeps everything already accepted. Re-submitting is safe: already-accepted items come back as per-item duplicate errors rather than running twice.

client.workflows.analyze_files(...) composes create, upload, submit and poll into one call and returns per-file outcomes. It adds no routes of its own. Use it for straightforward cases and the batch endpoints when you need control over the individual steps.

Limits to design around

Rate limits are enforced per user. A single service credential shared across a fleet shares one budget for all of it, which is the most common surprise when moving from a pilot to production volume.

Limit Scope Notes
Uploads per user, per day Each recording registered for upload draws on the daily budget
Batch submission per user Rate-limited per request, with a 500-item ceiling per request
Reads per user Sized for per-analysis polling, but prefer the batch status endpoint to per-analysis loops
Storage per organisation A ceiling in bytes, not a rolling quota — deleting recordings frees it
Credits per organisation Research grants and purchased credits, reserved at submission and settled at completion. client.batch.quote_analyses prices a batch and says which balance pays before you commit to it
Projects per organisation A flat cap

Query the live values rather than hard-coding them:

limits = await client.user.usage_limits()    # None means unlimited
usage = await client.user.usage_detailed()

Responses carry RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset. The SDK retries throttled requests with exponential backoff; quota and credit errors are raised immediately, because retrying them cannot succeed.

Errors

Every SDK error derives from YouSleepClientError, with one exception per failure mode:

from yousleep_common.client import (
    AnalysisWorkflowError,     # the analysis failed or was cancelled; carries logs
    AuthenticationError,       # 401
    InsufficientCreditsError,  # 402
    NotFoundError,             # 404
    QuotaExceededError,        # a usage quota was hit
    RateLimitError,            # 429, after automatic retries were exhausted
    ValidationError,           # 422
)

Batch responses carry the same vocabulary per item as a machine-readable error.code, so one error taxonomy covers both call styles.

What the platform does not do yet

The following are not available, so an integration should not depend on them:

  • No webhooks or result callbacks. Completion is discovered by polling; the batch status endpoint is the efficient way to do it.
  • No service accounts. A key belongs to a person's account and acts as it; an integration's key is owned by the person who created it.
  • No reading from a customer bucket in place, and no compute in a customer environment. Import copies the bytes in.
  • No cross-organisation sharing. A project is shared within its organisation only, and an account belongs to exactly one organisation. Collaborating with a second organisation needs a second account.

If an integration needs any of these, raise it with us early. Several are open design questions rather than settled decisions.