Partner Authentication

Partner-level login, API keys, the two-token flow, and securing your credentials

Authentication is at the partner level — not the user level

This is the single most important thing to understand before you design your integration.

POST /api/v1/login authenticates your organisation (the partner), using the partner_id and secret issued to you. It does not authenticate an end user. The token you receive represents one anonymous assessment session tied to your partner account — the API has no concept of your users, no user registry, and no per-user credentials.

The consequences for your architecture:

  1. Put the API behind your own user authentication. Anyone who can trigger a login consumes your partner quota and acts as your organisation. Your application must decide who is allowed to start an assessment (your own sign-in, session management, and access control) before it calls this API on their behalf.
  2. Keep partner_id, secret, and api_key server-side only. Never embed them in a browser bundle, mobile app binary, or any client-distributed code. The recommended pattern is a thin backend-for-frontend: your server performs /login and terms acceptance, holds the Healthily token in your user's session, and proxies (or mints short-lived access for) the chat calls.
  3. One login = one conversation. Each login creates a fresh session; the conversation state is bound to the token. To run a new assessment for the same (or a different) user, log in again. When a conversation has ended (conversation_ended: true), only the final state remains readable — a new assessment requires a new login.
  4. You own the user↔session mapping. If you need to associate an assessment with one of your users (history, audit, support), store the mapping in your own system. The API cannot do it for you.
  End user ──(your login / your session)──►  YOUR backend
                                              │  partner_id + secret + api_key
                                              ▼
                                        POST /api/v1/login
                                              │  access_token (session-scoped)
                                              ▼
                                    /terms → /initial → /chat → /report

Credentials

CredentialWhere it's usedNotes
api_keyx-api-key header on every requestIdentifies your partner account at the gateway; also drives rate limiting
partner_id + secretBody of POST /login onlyExchange for a session token
access_tokenAuthorization: Bearer <token> on all subsequent callsJWT; expires (see below)

The two-token flow

Login alone is not enough to start an assessment. The initial token is only authorised to read and accept the legal terms; accepting the terms returns an upgraded token that unlocks the medical endpoints. This enforces that consent is recorded before any health data is processed (see Legal & Consent).

Step 1 — Login

POST /api/v1/login
x-api-key: <api_key>
Content-Type: application/json

{
  "partner_id": "<partner_id>",
  "secret": "<secret>"
}

Example request:

curl -X POST https://portal.your.md/api/v1/login \
     -H "x-api-key: GrNh3vPVdckgA9mCHxdmaSM2ucaywMZ9HbRJ1qTa"\
     -H "Content-Type: application/json"\
     -d '
     {
      "partner_id": "jZtrRBkCGWDgTWZKPqrE7CM6U8oGCJbt",
      "secret": "imPG3xd94D18xLksG80LRFpLKERDpfdt"
     }'

Response 200:

{
  "access_token": "eyJRA.....3TQ",
  "token_type": "bearer",
  "expires_in": 1860
}
  • expires_in is in seconds (~30 minutes by default).
  • An optional X-Language header here selects the conversation language for the whole session (default en-US; must be enabled for your partner).
📘

Make sure you keep a record of the sid JWT claim. It is the only session identifier provided.

Step 2 — Fetch terms

GET /api/v1/terms
x-api-key: <api_key>
Authorization: Bearer <access_token>

Returns the legal content and its content_version (see Legal & Consent for the full payload and rendering requirements).

Step 3 — Accept terms → upgraded token

POST /api/v1/terms
x-api-key: <api_key>
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "content_version": "<content_version from step 2>",
  "all_policies_accepted": true
}

Response 200:

{
  "login_response": {
    "access_token": "eyJRB.....9XW",
    "token_type": "bearer",
    "expires_in": 1860
  },
  "onboarding_response": {
    "age_question":      { "type": "numeric", "messages": [ ... ], "constraints": { ... } },
    "gender_question":   { "type": "choice",  "messages": [ ... ], "choices": [ ... ] },
    "symptoms_question": { "type": "text",    "messages": [ ... ], "constraints": { ... } }
  }
}

Discard the original token and use login_response.access_token from here on. The onboarding_response conveniently bundles the copy, choices, and constraints for the three onboarding inputs (age, gender, initial symptom) so you can render your onboarding screen from API-provided content instead of hard-coding it.

Step 4 — Use the upgraded token

All assessment endpoints (/initial, /chat, /step-back, /report, /symptom-search, /articles, /analytics) require:

x-api-key: <api_key>
Authorization: Bearer <upgraded access_token>

Token expiry and session lifetime

  • Tokens expire after roughly 30 minutes (expires_in is authoritative — don't hard-code it).
  • Server-side conversation state has its own TTL (on the order of an hour). A consultation is designed to be completed in one sitting.
  • On 401 mid-conversation, the session is gone: re-login, re-accept terms, and start a new assessment. The API cannot resume an expired conversation, so surface this gracefully in your UI (e.g. "your session expired — restart the assessment").

What the token contains (for the curious)

The access token is a signed JWT carrying your partner id, a session id, the locked language, and an expiry. It is opaque to you — do not parse it, build logic on its claims, or attempt to mint or modify tokens. Signature verification happens server-side on every call.