{"openapi":"3.0.3","info":{"title":"Hi Doctor Partner API","version":"1.0","description":"The Hi Doctor Partner API lets a partner submit online medical consultations for its patients, follow their clinical review, answer the doctor's information requests and read the resulting prescriptions. Partner requests use Authorization: Bearer <api key> with a partner API key; test keys and live keys are separate. See https://docs.hi-doctor.ai/partners/overview for the partner guides.","contact":{"name":"Hi Doctor","email":"hello@hi-doctor.ai","url":"https://docs.hi-doctor.ai"}},"paths":{"/v1/partner/ping/":{"get":{"operationId":"ping","description":"Confirms that the key is valid and tells you which partner and mode it belongs to. Needs no scope.\n\n### Access, effects and retries\n**Ownership:** The API key's own partner and mode (test or live).\n\n**Prerequisites:** A partner API key; no scope is needed.\n\n**Effects:** None.\n\n**Retries:** Safe to retry.","summary":"Check an API key","tags":["Connectivity"],"security":[{"PartnerApiKey":[]}],"responses":{"200":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ping"},"examples":{"example-1":{"value":{"ok":true,"partner":"acme-health","mode":"test","key_id":"k7q2m9x4p1z8"},"summary":"Illustrative response example"}}}},"description":"Success."},"400":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"invalid_request","code":"brand_header_not_allowed","message":"Brand headers are not accepted on partner requests; the brand comes from the API key.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request is malformed. Partner routes also refuse the patient-app brand header with 400 brand_header_not_allowed, before authentication (no rate-limit headers)."},"401":{"headers":{"WWW-Authenticate":{"schema":{"type":"string"},"description":"The authentication scheme to use: Bearer realm=\"hi-doctor-partner\"."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"authentication_error","code":"not_authenticated","message":"Authentication credentials were not provided.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"authentication_error","code":"invalid_token","message":"The API key is not valid.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"authentication_error","code":"key_revoked","message":"The API key has been revoked.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"authentication_error","code":"key_expired","message":"The API key has expired.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"authentication_error","code":"partner_suspended","message":"The partner account is suspended.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The API key is missing, malformed, unknown, revoked or expired, or the partner account is suspended. The response carries WWW-Authenticate: Bearer realm=\"hi-doctor-partner\" and no rate-limit headers."},"403":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"permission_error","code":"partner_not_live","message":"Live mode is not enabled for this partner.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"A live key was used while live mode is not enabled for the partner. This operation needs no scope."},"404":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PartnerError"},{"$ref":"#/components/schemas/PartnerFlagOffError"}]},"examples":{"example-1":{"value":{"error":{"type":"not_found"}},"summary":"Illustrative response example"}}}},"description":"While the Partner API is switched off for the environment, every route answers 404 with only {\"error\": {\"type\": \"not_found\"}}, before authentication."},"429":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"rate_limited","code":"throttled","message":"Request was throttled. Expected available in 12 seconds.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"Too many requests; wait for the number of seconds in Retry-After."}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"set -eu\numask 077\n# Path values must be URL-encoded; query string excludes the leading question mark.\nurl=\"${HD_BASE_URL:?Set API origin}/v1/partner/ping/${HD_QUERY_STRING:+?${HD_QUERY_STRING}}\"\nprintf 'Accept: application/json\\n' > \"${HD_REQUEST_HEADERS_FILE:?Private header file}\"\nprintf 'Authorization: Bearer %s\\n' \"${HD_PARTNER_API_KEY:?Set partner API key}\" >> \"$HD_REQUEST_HEADERS_FILE\"\ncurl --silent --show-error --max-time 30 --request GET \\\n  --header \"@$HD_REQUEST_HEADERS_FILE\" \\\n  --dump-header \"${HD_HEADERS_FILE:?Private response header file}\" \\\n  --output \"${HD_RESPONSE_FILE:?Private response file}\" \\\n  --write-out '%{http_code}\\n%{content_type}\\n' \\\n  \"$url\"\n"},{"lang":"Python","label":"requests","source":"import os, json\nfrom pathlib import Path\nfrom urllib.parse import quote\nimport requests\nos.umask(0o077)\nurl = os.environ[\"HD_BASE_URL\"].rstrip(\"/\") + '/v1/partner/ping/'\nif os.environ.get(\"HD_QUERY_STRING\"): url += \"?\" + os.environ[\"HD_QUERY_STRING\"]\nheaders = {\"Accept\": 'application/json'}\nheaders[\"Authorization\"] = \"Bearer \" + os.environ[\"HD_PARTNER_API_KEY\"]\nkwargs = {}\nresponse = requests.request('GET', url, headers=headers, timeout=30, allow_redirects=False, **kwargs)\nPath(os.environ[\"HD_RESPONSE_FILE\"]).write_bytes(response.content)\nPath(os.environ[\"HD_HEADERS_FILE\"]).write_text(json.dumps(dict(response.headers)))\nprint(response.status_code)\nprint(response.headers.get(\"Content-Type\", \"\"))\n"},{"lang":"JavaScript","label":"fetch","source":"import { readFileSync, writeFileSync } from \"node:fs\";\nconst env = process.env;\nlet url = env.HD_BASE_URL.replace(/\\/$/, \"\") + \"/v1/partner/ping/\";\nif (env.HD_QUERY_STRING) url += \"?\" + env.HD_QUERY_STRING;\nconst headers = { \"Accept\": \"application/json\" };\nheaders.Authorization = \"Bearer \" + env.HD_PARTNER_API_KEY;\nconst options = { method: \"GET\", headers, redirect: \"manual\", signal: AbortSignal.timeout(30000) };\nconst response = await fetch(url, options);\nwriteFileSync(env.HD_RESPONSE_FILE, Buffer.from(await response.arrayBuffer()), {mode: 0o600});\nwriteFileSync(env.HD_HEADERS_FILE, JSON.stringify(Object.fromEntries(response.headers)), {mode: 0o600});\nconsole.log(response.status);\nconsole.log(response.headers.get(\"content-type\") ?? \"\");\n"}]}},"/v1/partner/products/":{"get":{"operationId":"list_products","description":"The products, plans, dosages and prices available to your partner account, one entry per product and country. schema_version and schema_hash identify the questionnaire to submit with. Needs no scope.\n\n### Access, effects and retries\n**Ownership:** The API key's own partner and mode (test or live).\n\n**Prerequisites:** A partner API key; no scope is needed.\n\n**Effects:** None.\n\n**Retries:** Safe to retry.","summary":"List products","parameters":[{"in":"query","name":"country","schema":{"type":"string"},"description":"Only products for this ISO 3166-1 alpha-2 country.","example":"ES"}],"tags":["Products"],"security":[{"PartnerApiKey":[]}],"responses":{"200":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductList"},"examples":{"example-1":{"value":{"object":"list","results":[{"product_id":"oral_finasteride","name":"Oral finasteride","category":"hair_loss","questionnaire_slug":"hair-growth","schema_version":"412","schema_hash":"<HASH>","country":"ES","plans":[{"plan_key":"one_off","supply_days":90}],"dosages":[{"dosage":"1 mg","label":"1 mg"}],"price":{"amount_cents":2900,"currency":"EUR"}}]},"summary":"Illustrative response example"}}}},"description":"Success."},"400":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"invalid_request","code":"invalid_request","message":"country must be an ISO 3166-1 alpha-2 code.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"invalid_request","code":"brand_header_not_allowed","message":"Brand headers are not accepted on partner requests; the brand comes from the API key.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request is malformed. Partner routes also refuse the patient-app brand header with 400 brand_header_not_allowed, before authentication (no rate-limit headers)."},"401":{"headers":{"WWW-Authenticate":{"schema":{"type":"string"},"description":"The authentication scheme to use: Bearer realm=\"hi-doctor-partner\"."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"authentication_error","code":"not_authenticated","message":"Authentication credentials were not provided.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"authentication_error","code":"invalid_token","message":"The API key is not valid.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"authentication_error","code":"key_revoked","message":"The API key has been revoked.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"authentication_error","code":"key_expired","message":"The API key has expired.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"authentication_error","code":"partner_suspended","message":"The partner account is suspended.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The API key is missing, malformed, unknown, revoked or expired, or the partner account is suspended. The response carries WWW-Authenticate: Bearer realm=\"hi-doctor-partner\" and no rate-limit headers."},"403":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"permission_error","code":"partner_not_live","message":"Live mode is not enabled for this partner.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"A live key was used while live mode is not enabled for the partner. This operation needs no scope."},"404":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PartnerError"},{"$ref":"#/components/schemas/PartnerFlagOffError"}]},"examples":{"example-1":{"value":{"error":{"type":"not_found"}},"summary":"Illustrative response example"}}}},"description":"While the Partner API is switched off for the environment, every route answers 404 with only {\"error\": {\"type\": \"not_found\"}}, before authentication."},"429":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"rate_limited","code":"throttled","message":"Request was throttled. Expected available in 12 seconds.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"Too many requests; wait for the number of seconds in Retry-After."}},"x-pagination":{"kind":"unpaginated_list_envelope"},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"set -eu\numask 077\n# Path values must be URL-encoded; query string excludes the leading question mark.\nurl=\"${HD_BASE_URL:?Set API origin}/v1/partner/products/${HD_QUERY_STRING:+?${HD_QUERY_STRING}}\"\nprintf 'Accept: application/json\\n' > \"${HD_REQUEST_HEADERS_FILE:?Private header file}\"\nprintf 'Authorization: Bearer %s\\n' \"${HD_PARTNER_API_KEY:?Set partner API key}\" >> \"$HD_REQUEST_HEADERS_FILE\"\ncurl --silent --show-error --max-time 30 --request GET \\\n  --header \"@$HD_REQUEST_HEADERS_FILE\" \\\n  --dump-header \"${HD_HEADERS_FILE:?Private response header file}\" \\\n  --output \"${HD_RESPONSE_FILE:?Private response file}\" \\\n  --write-out '%{http_code}\\n%{content_type}\\n' \\\n  \"$url\"\n"},{"lang":"Python","label":"requests","source":"import os, json\nfrom pathlib import Path\nfrom urllib.parse import quote\nimport requests\nos.umask(0o077)\nurl = os.environ[\"HD_BASE_URL\"].rstrip(\"/\") + '/v1/partner/products/'\nif os.environ.get(\"HD_QUERY_STRING\"): url += \"?\" + os.environ[\"HD_QUERY_STRING\"]\nheaders = {\"Accept\": 'application/json'}\nheaders[\"Authorization\"] = \"Bearer \" + os.environ[\"HD_PARTNER_API_KEY\"]\nkwargs = {}\nresponse = requests.request('GET', url, headers=headers, timeout=30, allow_redirects=False, **kwargs)\nPath(os.environ[\"HD_RESPONSE_FILE\"]).write_bytes(response.content)\nPath(os.environ[\"HD_HEADERS_FILE\"]).write_text(json.dumps(dict(response.headers)))\nprint(response.status_code)\nprint(response.headers.get(\"Content-Type\", \"\"))\n"},{"lang":"JavaScript","label":"fetch","source":"import { readFileSync, writeFileSync } from \"node:fs\";\nconst env = process.env;\nlet url = env.HD_BASE_URL.replace(/\\/$/, \"\") + \"/v1/partner/products/\";\nif (env.HD_QUERY_STRING) url += \"?\" + env.HD_QUERY_STRING;\nconst headers = { \"Accept\": \"application/json\" };\nheaders.Authorization = \"Bearer \" + env.HD_PARTNER_API_KEY;\nconst options = { method: \"GET\", headers, redirect: \"manual\", signal: AbortSignal.timeout(30000) };\nconst response = await fetch(url, options);\nwriteFileSync(env.HD_RESPONSE_FILE, Buffer.from(await response.arrayBuffer()), {mode: 0o600});\nwriteFileSync(env.HD_HEADERS_FILE, JSON.stringify(Object.fromEntries(response.headers)), {mode: 0o600});\nconsole.log(response.status);\nconsole.log(response.headers.get(\"content-type\") ?? \"\");\n"}]}},"/v1/partner/questionnaires/{slug}/":{"get":{"operationId":"get_questionnaire","description":"The published questionnaire for a product. Answer it as a list of entries keyed by question_key and send schema_version and schema_hash back with the consultation. Without locale the labels carry message keys only (each localizable field keeps its <field>Key either way). With locale the labels are written in that language, falling back to English (en) for an unsupported locale and per missing message, and no display field (a label, title or error text) carries a message key: text that no catalogue has is null, and its <field>Key still names it. The response's locale says which one was used and schema_hash is the hash of that localized schema. A submission accepts the current version's hash with or without a locale and in any supported locale. Needs no scope.\n\n### Access, effects and retries\n**Ownership:** The API key's own partner and mode (test or live); another partner's or mode's records answer 404.\n\n**Prerequisites:** A partner API key; no scope is needed.\n\n**Effects:** None.\n\n**Retries:** Safe to retry.","summary":"Get a questionnaire","parameters":[{"in":"path","name":"slug","schema":{"type":"string"},"description":"The questionnaire slug.","required":true,"example":"hair-growth"},{"in":"query","name":"product_id","schema":{"type":"string"},"description":"Pick the questionnaire variant for this product.","example":"oral_finasteride"},{"in":"query","name":"locale","schema":{"type":"string"},"description":"Language for the labels: one of en, es, zh. Any other value falls back to English.","example":"es"}],"tags":["Questionnaires"],"security":[{"PartnerApiKey":[]}],"responses":{"200":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Questionnaire"},"examples":{"example-1":{"value":{"slug":"hair-growth","schema_version":"412","schema_hash":"<HASH>","locale":null,"answer_format":"entries-v1","upload_answer":{"value":"<upload_id>"},"schema":{"steps":[{"key":"hair_loss_duration","kind":"single","label":"categories.hairGrowth.form.steps.duration.label","labelKey":"categories.hairGrowth.form.steps.duration.label","required":true,"options":[{"value":"under_1_year","label":"categories.hairGrowth.form.steps.duration.options.under1Year","labelKey":"categories.hairGrowth.form.steps.duration.options.under1Year"},{"value":"over_1_year","label":"categories.hairGrowth.form.steps.duration.options.over1Year","labelKey":"categories.hairGrowth.form.steps.duration.options.over1Year"}]}]}},"summary":"Illustrative response example"},"example-2":{"value":{"slug":"hair-growth","schema_version":"412","schema_hash":"<HASH>","locale":"es","answer_format":"entries-v1","upload_answer":{"value":"<upload_id>"},"schema":{"steps":[{"key":"hair_loss_duration","kind":"single","label":"¿Desde cuándo notas la caída del pelo?","labelKey":"categories.hairGrowth.form.steps.duration.label","required":true,"options":[{"value":"under_1_year","label":"Menos de un año","labelKey":"categories.hairGrowth.form.steps.duration.options.under1Year"},{"value":"over_1_year","label":"Más de un año","labelKey":"categories.hairGrowth.form.steps.duration.options.over1Year"}]}]}},"summary":"Illustrative response example"}}}},"description":"Success."},"400":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"invalid_request","code":"brand_header_not_allowed","message":"Brand headers are not accepted on partner requests; the brand comes from the API key.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request is malformed. Partner routes also refuse the patient-app brand header with 400 brand_header_not_allowed, before authentication (no rate-limit headers)."},"401":{"headers":{"WWW-Authenticate":{"schema":{"type":"string"},"description":"The authentication scheme to use: Bearer realm=\"hi-doctor-partner\"."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"authentication_error","code":"not_authenticated","message":"Authentication credentials were not provided.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"authentication_error","code":"invalid_token","message":"The API key is not valid.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"authentication_error","code":"key_revoked","message":"The API key has been revoked.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"authentication_error","code":"key_expired","message":"The API key has expired.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"authentication_error","code":"partner_suspended","message":"The partner account is suspended.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The API key is missing, malformed, unknown, revoked or expired, or the partner account is suspended. The response carries WWW-Authenticate: Bearer realm=\"hi-doctor-partner\" and no rate-limit headers."},"403":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"permission_error","code":"partner_not_live","message":"Live mode is not enabled for this partner.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"A live key was used while live mode is not enabled for the partner. This operation needs no scope."},"404":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PartnerError"},{"$ref":"#/components/schemas/PartnerFlagOffError"}]},"examples":{"example-1":{"value":{"error":{"type":"not_found","code":"not_found","message":"No such resource.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"not_found"}},"summary":"Illustrative response example"}}}},"description":"No such resource for this key's partner and mode. While the Partner API is switched off for the environment, every route answers 404 with only {\"error\": {\"type\": \"not_found\"}}, before authentication."},"429":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"rate_limited","code":"throttled","message":"Request was throttled. Expected available in 12 seconds.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"Too many requests; wait for the number of seconds in Retry-After."}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"set -eu\numask 077\n# Path values must be URL-encoded; query string excludes the leading question mark.\nurl=\"${HD_BASE_URL:?Set API origin}/v1/partner/questionnaires/${HD_PATH_SLUG:?Set the path fixture}/${HD_QUERY_STRING:+?${HD_QUERY_STRING}}\"\nprintf 'Accept: application/json\\n' > \"${HD_REQUEST_HEADERS_FILE:?Private header file}\"\nprintf 'Authorization: Bearer %s\\n' \"${HD_PARTNER_API_KEY:?Set partner API key}\" >> \"$HD_REQUEST_HEADERS_FILE\"\ncurl --silent --show-error --max-time 30 --request GET \\\n  --header \"@$HD_REQUEST_HEADERS_FILE\" \\\n  --dump-header \"${HD_HEADERS_FILE:?Private response header file}\" \\\n  --output \"${HD_RESPONSE_FILE:?Private response file}\" \\\n  --write-out '%{http_code}\\n%{content_type}\\n' \\\n  \"$url\"\n"},{"lang":"Python","label":"requests","source":"import os, json\nfrom pathlib import Path\nfrom urllib.parse import quote\nimport requests\nos.umask(0o077)\nurl = os.environ[\"HD_BASE_URL\"].rstrip(\"/\") + '/v1/partner/questionnaires/{slug}/'\nurl = url.replace('{slug}', quote(os.environ['HD_PATH_SLUG'], safe=\"\"))\nif os.environ.get(\"HD_QUERY_STRING\"): url += \"?\" + os.environ[\"HD_QUERY_STRING\"]\nheaders = {\"Accept\": 'application/json'}\nheaders[\"Authorization\"] = \"Bearer \" + os.environ[\"HD_PARTNER_API_KEY\"]\nkwargs = {}\nresponse = requests.request('GET', url, headers=headers, timeout=30, allow_redirects=False, **kwargs)\nPath(os.environ[\"HD_RESPONSE_FILE\"]).write_bytes(response.content)\nPath(os.environ[\"HD_HEADERS_FILE\"]).write_text(json.dumps(dict(response.headers)))\nprint(response.status_code)\nprint(response.headers.get(\"Content-Type\", \"\"))\n"},{"lang":"JavaScript","label":"fetch","source":"import { readFileSync, writeFileSync } from \"node:fs\";\nconst env = process.env;\nlet url = env.HD_BASE_URL.replace(/\\/$/, \"\") + \"/v1/partner/questionnaires/{slug}/\";\nurl = url.replace(\"{slug}\", encodeURIComponent(env.HD_PATH_SLUG));\nif (env.HD_QUERY_STRING) url += \"?\" + env.HD_QUERY_STRING;\nconst headers = { \"Accept\": \"application/json\" };\nheaders.Authorization = \"Bearer \" + env.HD_PARTNER_API_KEY;\nconst options = { method: \"GET\", headers, redirect: \"manual\", signal: AbortSignal.timeout(30000) };\nconst response = await fetch(url, options);\nwriteFileSync(env.HD_RESPONSE_FILE, Buffer.from(await response.arrayBuffer()), {mode: 0o600});\nwriteFileSync(env.HD_HEADERS_FILE, JSON.stringify(Object.fromEntries(response.headers)), {mode: 0o600});\nconsole.log(response.status);\nconsole.log(response.headers.get(\"content-type\") ?? \"\");\n"}]}},"/v1/partner/files/":{"post":{"operationId":"upload_file","description":"Uploads one file as multipart/form-data and returns an upload_id. Use the id as the answer to an upload question or in an information request answer. Pending uploads expire after 24 hours. Pending uploads count towards your storage quota (refused with 422 storage_quota_exceeded when full); an expired upload stops counting at expires_at, can no longer be used (422 upload_unusable) and is deleted, file included, by a periodic cleanup. Attached uploads are kept with the consultation. Requires the consultations:write scope.\n\n### Access, effects and retries\n**Ownership:** The API key's own partner and mode (test or live).\n\n**Prerequisites:** A partner API key with the `consultations:write` scope.\n\n**Effects:** Stores one pending upload that counts towards the storage quota until it is attached or expires.\n\n**Retries:** Not replayed by Idempotency-Key: a retry stores a second upload. Unused uploads expire and are cleaned up.","summary":"Upload a photo or document","parameters":[{"in":"header","name":"Idempotency-Key","schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{8,128}$"},"description":"Accepted but not used for replay on this operation. Only POST /v1/partner/consultations stores and replays responses by Idempotency-Key.","example":"b7c1d2e3f4a5b6c7"}],"tags":["Files"],"requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/FileUploadRequest"},"examples":{"example-1":{"summary":"Illustrative request example","value":{"file":"<JPEG, PNG or PDF bytes>","purpose":"photo"}}}}},"required":true},"security":[{"PartnerApiKey":[]}],"responses":{"201":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Upload"},"examples":{"example-1":{"value":{"upload_id":"3d4e5f60-7182-4934-a5b6-c7d8e9f0a1b2","object":"upload","purpose":"photo","content_type":"image/jpeg","bytes":482113,"sha256":"<HASH>","width":1600,"height":1200,"status":"pending","expires_at":"2026-11-04T10:15:00Z"},"summary":"Illustrative response example"}}}},"description":"The stored upload."},"400":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"invalid_request","code":"parse_error","message":"Multipart form parse error - Invalid boundary in multipart: None","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"invalid_request","code":"brand_header_not_allowed","message":"Brand headers are not accepted on partner requests; the brand comes from the API key.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request is malformed. Partner routes also refuse the patient-app brand header with 400 brand_header_not_allowed, before authentication (no rate-limit headers)."},"401":{"headers":{"WWW-Authenticate":{"schema":{"type":"string"},"description":"The authentication scheme to use: Bearer realm=\"hi-doctor-partner\"."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"authentication_error","code":"not_authenticated","message":"Authentication credentials were not provided.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"authentication_error","code":"invalid_token","message":"The API key is not valid.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"authentication_error","code":"key_revoked","message":"The API key has been revoked.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"authentication_error","code":"key_expired","message":"The API key has expired.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"authentication_error","code":"partner_suspended","message":"The partner account is suspended.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The API key is missing, malformed, unknown, revoked or expired, or the partner account is suspended. The response carries WWW-Authenticate: Bearer realm=\"hi-doctor-partner\" and no rate-limit headers."},"403":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"permission_error","code":"insufficient_scope","message":"This endpoint requires the consultations:write scope.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"permission_error","code":"partner_not_live","message":"Live mode is not enabled for this partner.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The key lacks the consultations:write scope (insufficient_scope), or a live key was used while live mode is not enabled for the partner (partner_not_live)."},"404":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PartnerError"},{"$ref":"#/components/schemas/PartnerFlagOffError"}]},"examples":{"example-1":{"value":{"error":{"type":"not_found"}},"summary":"Illustrative response example"}}}},"description":"While the Partner API is switched off for the environment, every route answers 404 with only {\"error\": {\"type\": \"not_found\"}}, before authentication."},"422":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"validation_error","code":"invalid_request","message":"Send a multipart 'file' and a purpose of photo or document.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"validation_error","code":"unsupported_media","message":"Only JPEG, PNG and PDF files are accepted.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"validation_error","code":"file_too_large","message":"The file exceeds 10 MB.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"validation_error","code":"image_too_large","message":"The image dimensions exceed the allowed limits.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"validation_error","code":"checksum_mismatch","message":"The sha256 does not match the uploaded bytes.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-6":{"value":{"error":{"type":"validation_error","code":"storage_quota_exceeded","message":"The partner's pending upload storage is full.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request body is invalid."},"429":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"rate_limited","code":"throttled","message":"Request was throttled. Expected available in 12 seconds.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"Too many requests; wait for the number of seconds in Retry-After."}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"set -eu\numask 077\n# Path values must be URL-encoded; query string excludes the leading question mark.\nurl=\"${HD_BASE_URL:?Set API origin}/v1/partner/files/${HD_QUERY_STRING:+?${HD_QUERY_STRING}}\"\nprintf 'Accept: application/json\\n' > \"${HD_REQUEST_HEADERS_FILE:?Private header file}\"\nprintf 'Authorization: Bearer %s\\n' \"${HD_PARTNER_API_KEY:?Set partner API key}\" >> \"$HD_REQUEST_HEADERS_FILE\"\nform_args=()\nwhile IFS= read -r -d \"\" form_value; do\n  form_args+=(--form-string \"$form_value\")\ndone < <(python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); [sys.stdout.write(str(k)+\"=\"+str(v)+\"\\0\") for k,v in d.items() if v is not None]' \"${HD_REQUEST_FILE:?JSON form fields}\")\ncurl --silent --show-error --max-time 30 --request POST \\\n  --header \"@$HD_REQUEST_HEADERS_FILE\" \\\n  --dump-header \"${HD_HEADERS_FILE:?Private response header file}\" \\\n  --output \"${HD_RESPONSE_FILE:?Private response file}\" \\\n  --write-out '%{http_code}\\n%{content_type}\\n' \\\n  --form 'file=@'\"${HD_UPLOAD_FILE:?Synthetic upload};type=${HD_UPLOAD_CONTENT_TYPE:?Upload MIME}\" \\\n  ${form_args[@]+\"${form_args[@]}\"} \\\n  \"$url\"\n"},{"lang":"Python","label":"requests","source":"import os, json\nfrom pathlib import Path\nfrom urllib.parse import quote\nimport requests\nos.umask(0o077)\nurl = os.environ[\"HD_BASE_URL\"].rstrip(\"/\") + '/v1/partner/files/'\nif os.environ.get(\"HD_QUERY_STRING\"): url += \"?\" + os.environ[\"HD_QUERY_STRING\"]\nheaders = {\"Accept\": 'application/json'}\nheaders[\"Authorization\"] = \"Bearer \" + os.environ[\"HD_PARTNER_API_KEY\"]\nkwargs = {}\nkwargs[\"files\"] = {'file': (\"synthetic-upload\", Path(os.environ[\"HD_UPLOAD_FILE\"]).read_bytes(), os.environ[\"HD_UPLOAD_CONTENT_TYPE\"])}\nkwargs[\"data\"] = json.loads(Path(os.environ[\"HD_REQUEST_FILE\"]).read_text())\nresponse = requests.request('POST', url, headers=headers, timeout=30, allow_redirects=False, **kwargs)\nPath(os.environ[\"HD_RESPONSE_FILE\"]).write_bytes(response.content)\nPath(os.environ[\"HD_HEADERS_FILE\"]).write_text(json.dumps(dict(response.headers)))\nprint(response.status_code)\nprint(response.headers.get(\"Content-Type\", \"\"))\n"},{"lang":"JavaScript","label":"fetch","source":"import { readFileSync, writeFileSync } from \"node:fs\";\nconst env = process.env;\nlet url = env.HD_BASE_URL.replace(/\\/$/, \"\") + \"/v1/partner/files/\";\nif (env.HD_QUERY_STRING) url += \"?\" + env.HD_QUERY_STRING;\nconst headers = { \"Accept\": \"application/json\" };\nheaders.Authorization = \"Bearer \" + env.HD_PARTNER_API_KEY;\nconst options = { method: \"POST\", headers, redirect: \"manual\", signal: AbortSignal.timeout(30000) };\noptions.body = new FormData();\noptions.body.append(\"file\", new Blob([readFileSync(env.HD_UPLOAD_FILE)], {type: env.HD_UPLOAD_CONTENT_TYPE}), \"synthetic-upload\");\nfor (const [key, value] of Object.entries(JSON.parse(readFileSync(env.HD_REQUEST_FILE, \"utf8\")))) { if (value != null) options.body.append(key, String(value)); }\nconst response = await fetch(url, options);\nwriteFileSync(env.HD_RESPONSE_FILE, Buffer.from(await response.arrayBuffer()), {mode: 0o600});\nwriteFileSync(env.HD_HEADERS_FILE, JSON.stringify(Object.fromEntries(response.headers)), {mode: 0o600});\nconsole.log(response.status);\nconsole.log(response.headers.get(\"content-type\") ?? \"\");\n"}]}},"/v1/partner/consultations/":{"get":{"operationId":"list_consultations","description":"Your consultations in this key's mode, newest first, with cursor pagination. Requires the consultations:read scope.\n\n### Access, effects and retries\n**Ownership:** The API key's own partner and mode (test or live).\n\n**Prerequisites:** A partner API key with the `consultations:read` scope.\n\n**Effects:** None.\n\n**Retries:** Safe to retry.","summary":"List consultations","parameters":[{"in":"query","name":"status","schema":{"type":"string","enum":["approved","cancelled","in_review","info_requested","rejected","submitted"]},"description":"Only consultations in this status.","example":"in_review"},{"in":"query","name":"external_patient_id","schema":{"type":"string"},"description":"Only consultations for the patient with this external_patient_id from your system.","example":"cust-8812"},{"in":"query","name":"external_reference","schema":{"type":"string"},"description":"Only the consultation with this external_reference from your system.","example":"order-10045"},{"in":"query","name":"created_after","schema":{"type":"string","format":"date-time"},"description":"Inclusive.","example":"2026-11-01T00:00:00Z"},{"in":"query","name":"created_before","schema":{"type":"string","format":"date-time"},"description":"Exclusive.","example":"2026-11-04T00:00:00Z"},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Page size, 1 to 100. Default 50. A value outside the range is refused with 400 invalid_request.","example":50},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"The next_cursor of the previous page.","example":"MjAyNi0xMS0wM1QxMDoxNTowMCswMDowMHw3ZjhiMWMyZS00ZDVhLTRlNmYtOWEwYi0xYzJkM2U0ZjVhNmI="}],"tags":["Consultations"],"security":[{"PartnerApiKey":[]}],"responses":{"200":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConsultationList"},"examples":{"example-1":{"value":{"object":"list","results":[{"id":"7f8b1c2e-4d5a-4e6f-9a0b-1c2d3e4f5a6b","object":"consultation","mode":"test","status":"approved","version":4,"status_history":[{"status":"submitted","version":1,"at":"2026-11-03T10:15:00Z"},{"status":"in_review","version":2,"at":"2026-11-03T11:02:00Z"},{"status":"approved","version":3,"at":"2026-11-03T12:40:00Z"}],"external_reference":"order-10045","product":{"product_id":"oral_finasteride","name":"Oral finasteride"},"country":"ES","plan_key":"one_off","dosage":"1 mg","patient":{"id":"0e1d2c3b-4a59-4687-8b9a-0b1c2d3e4f50","external_patient_id":"cust-8812"},"submitted_at":"2026-11-03T10:15:00Z","updated_at":"2026-11-03T12:45:00Z","reviewer":{"description":"Hi Doctor EU-licensed doctor","licence_country":"ES"},"decision":{"outcome":"approved","reason_category":null,"decided_at":"2026-11-03T12:40:00Z"},"open_info_request":null,"prescriptions":[{"id":"5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d","consultation_id":"7f8b1c2e-4d5a-4e6f-9a0b-1c2d3e4f5a6b","status":"issued","active":true,"issued_at":"2026-11-03T12:45:00Z","cancelled_at":null,"expires_at":null,"dispensing_country":null,"medication":{"name":"oral_finasteride","prescribed_label":"oral_finasteride","dosage":"1 mg","supply_days":null},"fulfilment":{"route":"cross_border_pdf"},"collected_at":null,"replaced_by":null}],"billing":{"state":"billable","price":{"amount_cents":2900,"currency":"EUR"}},"cancellable":false}],"next_cursor":"MjAyNi0xMS0wM1QxMDoxNTowMCswMDowMHw3ZjhiMWMyZS00ZDVhLTRlNmYtOWEwYi0xYzJkM2U0ZjVhNmI="},"summary":"Illustrative response example"}}}},"description":"Success."},"400":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"invalid_request","code":"invalid_request","message":"limit must be between 1 and 100.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"invalid_request","code":"invalid_cursor","message":"The pagination cursor is not valid.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"invalid_request","code":"invalid_request","message":"created_after must be an ISO 8601 date-time.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"invalid_request","code":"brand_header_not_allowed","message":"Brand headers are not accepted on partner requests; the brand comes from the API key.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request is malformed. Partner routes also refuse the patient-app brand header with 400 brand_header_not_allowed, before authentication (no rate-limit headers)."},"401":{"headers":{"WWW-Authenticate":{"schema":{"type":"string"},"description":"The authentication scheme to use: Bearer realm=\"hi-doctor-partner\"."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"authentication_error","code":"not_authenticated","message":"Authentication credentials were not provided.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"authentication_error","code":"invalid_token","message":"The API key is not valid.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"authentication_error","code":"key_revoked","message":"The API key has been revoked.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"authentication_error","code":"key_expired","message":"The API key has expired.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"authentication_error","code":"partner_suspended","message":"The partner account is suspended.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The API key is missing, malformed, unknown, revoked or expired, or the partner account is suspended. The response carries WWW-Authenticate: Bearer realm=\"hi-doctor-partner\" and no rate-limit headers."},"403":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"permission_error","code":"insufficient_scope","message":"This endpoint requires the consultations:read scope.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"permission_error","code":"partner_not_live","message":"Live mode is not enabled for this partner.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The key lacks the consultations:read scope (insufficient_scope), or a live key was used while live mode is not enabled for the partner (partner_not_live)."},"404":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PartnerError"},{"$ref":"#/components/schemas/PartnerFlagOffError"}]},"examples":{"example-1":{"value":{"error":{"type":"not_found"}},"summary":"Illustrative response example"}}}},"description":"While the Partner API is switched off for the environment, every route answers 404 with only {\"error\": {\"type\": \"not_found\"}}, before authentication."},"429":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"rate_limited","code":"throttled","message":"Request was throttled. Expected available in 12 seconds.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"Too many requests; wait for the number of seconds in Retry-After."}},"x-pagination":{"kind":"cursor","defaultLimit":50,"maxLimit":100,"ordering":["-created_at"],"defaultOrdering":"-created_at"},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"set -eu\numask 077\n# Path values must be URL-encoded; query string excludes the leading question mark.\nurl=\"${HD_BASE_URL:?Set API origin}/v1/partner/consultations/${HD_QUERY_STRING:+?${HD_QUERY_STRING}}\"\nprintf 'Accept: application/json\\n' > \"${HD_REQUEST_HEADERS_FILE:?Private header file}\"\nprintf 'Authorization: Bearer %s\\n' \"${HD_PARTNER_API_KEY:?Set partner API key}\" >> \"$HD_REQUEST_HEADERS_FILE\"\ncurl --silent --show-error --max-time 30 --request GET \\\n  --header \"@$HD_REQUEST_HEADERS_FILE\" \\\n  --dump-header \"${HD_HEADERS_FILE:?Private response header file}\" \\\n  --output \"${HD_RESPONSE_FILE:?Private response file}\" \\\n  --write-out '%{http_code}\\n%{content_type}\\n' \\\n  \"$url\"\n"},{"lang":"Python","label":"requests","source":"import os, json\nfrom pathlib import Path\nfrom urllib.parse import quote\nimport requests\nos.umask(0o077)\nurl = os.environ[\"HD_BASE_URL\"].rstrip(\"/\") + '/v1/partner/consultations/'\nif os.environ.get(\"HD_QUERY_STRING\"): url += \"?\" + os.environ[\"HD_QUERY_STRING\"]\nheaders = {\"Accept\": 'application/json'}\nheaders[\"Authorization\"] = \"Bearer \" + os.environ[\"HD_PARTNER_API_KEY\"]\nkwargs = {}\nresponse = requests.request('GET', url, headers=headers, timeout=30, allow_redirects=False, **kwargs)\nPath(os.environ[\"HD_RESPONSE_FILE\"]).write_bytes(response.content)\nPath(os.environ[\"HD_HEADERS_FILE\"]).write_text(json.dumps(dict(response.headers)))\nprint(response.status_code)\nprint(response.headers.get(\"Content-Type\", \"\"))\n"},{"lang":"JavaScript","label":"fetch","source":"import { readFileSync, writeFileSync } from \"node:fs\";\nconst env = process.env;\nlet url = env.HD_BASE_URL.replace(/\\/$/, \"\") + \"/v1/partner/consultations/\";\nif (env.HD_QUERY_STRING) url += \"?\" + env.HD_QUERY_STRING;\nconst headers = { \"Accept\": \"application/json\" };\nheaders.Authorization = \"Bearer \" + env.HD_PARTNER_API_KEY;\nconst options = { method: \"GET\", headers, redirect: \"manual\", signal: AbortSignal.timeout(30000) };\nconst response = await fetch(url, options);\nwriteFileSync(env.HD_RESPONSE_FILE, Buffer.from(await response.arrayBuffer()), {mode: 0o600});\nwriteFileSync(env.HD_HEADERS_FILE, JSON.stringify(Object.fromEntries(response.headers)), {mode: 0o600});\nconsole.log(response.status);\nconsole.log(response.headers.get(\"content-type\") ?? \"\");\n"}]},"post":{"operationId":"create_consultation","description":"Submits a patient's questionnaire answers for clinical review. The patient's consent flags must all be true and the notice version must be the current one (otherwise 422 consent_notice_outdated with current_notice_version). schema_version and schema_hash must name the published questionnaire (otherwise 409 schema_outdated with the current pair). Retries with the same Idempotency-Key and body return the original response.\n\nAnswer only the questions that apply after branching: an answer to a step the other answers hide (conditional display) is refused with 422 unexpected_question_key, one error item per hidden question_key. Errors never repeat a submitted answer value or identifier; they name the question_key instead (an unexpected question_key is echoed as sent). A pharmacy selection that is not valid for the patient's country is refused with 422 pharmacy_selection_invalid (question_key pharmacy_selection), whatever the directory's reason.\n\npatient.national_identifier is optional: send it where the product country's prescribing needs one (otherwise 422 patient_identifier_required, unless the identifier is already stored for the patient). An unknown type or invalid value is refused with 422 invalid_request (field patient.national_identifier, code invalid_identifier); it is never echoed or returned. In live mode it is stored on the patient's account the first time; a different identifier for the same patient is refused with 409 patient_conflict and never overwrites it. Test mode validates it and stores nothing.\n\npatient.identity_document is optional: send it where electronic prescribing in the patient's country needs the patient's identity document before approval (a national id_card or a passport, with the country that issued it). It is validated like the patient's own profile (5 to 20 letters or digits; for a few issuing countries an id_card number is also checked against its national format); one that breaks those rules is refused with 422 invalid_request (field patient.identity_document, code invalid_identity_document). Structural errors keep their nested field errors (for example field patient.identity_document.type, code invalid_choice, for an unsupported type; patient.identity_document.number, code required, when the number is missing). It is never echoed or returned. In live mode it is stored on the patient's account the first time; a different document for the same patient is refused with 409 patient_conflict and never overwrites it. Test mode validates it and stores nothing. Without it, the reviewing doctor, if they need it, asks through an information request with reason_code identity_document_required.\n\npatient.address line1 and line2 are stored joined with \", \" in one street line of at most 200 characters (separator included); a longer address is refused with 422 invalid_request (field patient.address), never truncated. patient.address.country is the patient's country of residence and must be an EU member state (ISO 3166-1 alpha-2 or alpha-3), in test and live mode alike; any other value is refused with 422 invalid_request (field patient.address.country, code residence_outside_eu). Without it, the product country is the residence. A patient who withdrew consent is refused with 409 consent_withdrawn. Requires the consultations:write scope.\n\n### Access, effects and retries\n**Ownership:** The API key's own partner and mode (test or live).\n\n**Prerequisites:** A partner API key with the `consultations:write` scope.\n\n**Effects:** Creates the consultation (and the patient on first submission) and sends it to clinical review; emits consultation webhook events.\n\n**Retries:** Required Idempotency-Key: a retry with the same key and body within 24 hours returns the stored response.","summary":"Submit a consultation","parameters":[{"in":"header","name":"Idempotency-Key","schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{8,128}$"},"description":"A unique key per logical request. Retrying with the same key and body returns the stored response for 24 hours instead of creating a second consultation; the same key with a different body is refused.","required":true,"example":"order-10045-submit-1"}],"tags":["Consultations"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConsultationCreateRequest"},"examples":{"example-1":{"summary":"Illustrative request example","value":{"product_id":"oral_finasteride","country":"ES","plan_key":"one_off","dosage":"1 mg","locale":"es","schema_version":"412","schema_hash":"<HASH>","external_reference":"order-10045","patient":{"external_patient_id":"cust-8812","email":"patient@example.com","first_name":"Sam","last_name":"Example","date_of_birth":"1988-04-02","sex":"male","phone":"+34600000000","address":{"line1":"Calle Example 1","line2":"2B","city":"Madrid","postal_code":"28001","country":"ES"},"national_identifier":{"type":"dni","value":"12345678Z"},"consent":{"terms_accepted":true,"privacy_accepted":true,"clinical_data_processing_accepted":true,"accepted_at":"2026-11-03T10:10:00Z","notice_version":"pn-2026-11","evidence_reference":"consent-log-55102","ip":"203.0.113.24"}},"answers":[{"question_key":"hair_loss_duration","value":"over_1_year"}]}}}}},"required":true},"security":[{"PartnerApiKey":[]}],"responses":{"201":{"headers":{"Idempotent-Replayed":{"schema":{"type":"string","enum":["true"]},"description":"true when the response is the stored one, replayed for a retry with the same Idempotency-Key and body; absent on the original response."},"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Consultation"},"examples":{"example-1":{"value":{"id":"7f8b1c2e-4d5a-4e6f-9a0b-1c2d3e4f5a6b","object":"consultation","mode":"test","status":"submitted","version":1,"status_history":[{"status":"submitted","version":1,"at":"2026-11-03T10:15:00Z"}],"external_reference":"order-10045","product":{"product_id":"oral_finasteride","name":"Oral finasteride"},"country":"ES","plan_key":"one_off","dosage":"1 mg","patient":{"id":"0e1d2c3b-4a59-4687-8b9a-0b1c2d3e4f50","external_patient_id":"cust-8812"},"submitted_at":"2026-11-03T10:15:00Z","updated_at":"2026-11-03T10:15:00Z","reviewer":null,"decision":null,"open_info_request":null,"prescriptions":[],"billing":{"state":"pending","price":{"amount_cents":2900,"currency":"EUR"}},"cancellable":true},"summary":"Illustrative response example"}}}},"description":"The new consultation."},"400":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"invalid_request","code":"parse_error","message":"JSON parse error - Expecting value: line 1 column 1 (char 0)","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"invalid_request","code":"idempotency_key_required","message":"This endpoint requires an Idempotency-Key header.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"invalid_request","code":"invalid_idempotency_key","message":"Idempotency-Key must match [A-Za-z0-9_-]{8,128}.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"invalid_request","code":"brand_header_not_allowed","message":"Brand headers are not accepted on partner requests; the brand comes from the API key.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request is malformed. Partner routes also refuse the patient-app brand header with 400 brand_header_not_allowed, before authentication (no rate-limit headers)."},"401":{"headers":{"WWW-Authenticate":{"schema":{"type":"string"},"description":"The authentication scheme to use: Bearer realm=\"hi-doctor-partner\"."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"authentication_error","code":"not_authenticated","message":"Authentication credentials were not provided.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"authentication_error","code":"invalid_token","message":"The API key is not valid.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"authentication_error","code":"key_revoked","message":"The API key has been revoked.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"authentication_error","code":"key_expired","message":"The API key has expired.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"authentication_error","code":"partner_suspended","message":"The partner account is suspended.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The API key is missing, malformed, unknown, revoked or expired, or the partner account is suspended. The response carries WWW-Authenticate: Bearer realm=\"hi-doctor-partner\" and no rate-limit headers."},"403":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"permission_error","code":"insufficient_scope","message":"This endpoint requires the consultations:write scope.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"permission_error","code":"partner_not_live","message":"Live mode is not enabled for this partner.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The key lacks the consultations:write scope (insufficient_scope), or a live key was used while live mode is not enabled for the partner (partner_not_live)."},"404":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PartnerError"},{"$ref":"#/components/schemas/PartnerFlagOffError"}]},"examples":{"example-1":{"value":{"error":{"type":"not_found"}},"summary":"Illustrative response example"}}}},"description":"While the Partner API is switched off for the environment, every route answers 404 with only {\"error\": {\"type\": \"not_found\"}}, before authentication."},"409":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"conflict","code":"duplicate_external_reference","message":"A consultation with this external_reference already exists.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"conflict","code":"patient_conflict","message":"An account with this email already exists for the brand.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"conflict","code":"patient_conflict","message":"The national identifier does not match the patient's account.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"conflict","code":"patient_conflict","message":"The identity document does not match the patient's account.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"conflict","code":"schema_outdated","message":"The questionnaire schema has changed; fetch it again.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8","current":{"schema_version":"413","schema_hash":"<HASH>"}}},"summary":"Illustrative response example"},"example-6":{"value":{"error":{"type":"conflict","code":"consent_withdrawn","message":"The patient withdrew consent; no further consultations are processed.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-7":{"value":{"error":{"type":"conflict","code":"idempotency_in_progress","message":"A request with this Idempotency-Key is still executing.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-8":{"value":{"error":{"type":"conflict","code":"context_unsupported","message":"The submission needs a context the partner API does not offer.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request conflicts with the resource's current state."},"422":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"validation_error","code":"invalid_request","message":"The request body is invalid.","errors":[{"field":"answers","code":"required","message":"This field is required."}],"request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"validation_error","code":"invalid_request","message":"The request body is invalid.","errors":[{"field":"patient.national_identifier","code":"invalid_identifier","message":"Enter a valid identifier of a type the product country uses."}],"request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"validation_error","code":"invalid_request","message":"The request body is invalid.","errors":[{"field":"patient.identity_document","code":"invalid_identity_document","message":"Enter a valid document number for this document type and issuing state."}],"request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"validation_error","code":"invalid_request","message":"The request body is invalid.","errors":[{"field":"patient.identity_document.type","code":"invalid_choice","message":"This is not a valid choice."}],"request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"validation_error","code":"invalid_request","message":"The request body is invalid.","errors":[{"field":"patient.address","code":"max_length","message":"line1 and line2 together must not exceed 200 characters."}],"request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-6":{"value":{"error":{"type":"validation_error","code":"invalid_request","message":"The request body is invalid.","errors":[{"field":"patient.address.country","code":"residence_outside_eu","message":"Country of residence must be within the EU"}],"request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-7":{"value":{"error":{"type":"validation_error","code":"unexpected_question_key","message":"Only questions that apply after branching may be answered.","errors":[{"question_key":"hair_loss_pattern","code":"unexpected_question_key","message":"Only questions that apply after branching may be answered."}],"request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-8":{"value":{"error":{"type":"validation_error","code":"invalid_answers","message":"The answer to question hair_loss_duration does not match the questionnaire schema.","errors":[{"question_key":"hair_loss_duration","code":"invalid_answers","message":"The answer to question hair_loss_duration does not match the questionnaire schema."}],"request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-9":{"value":{"error":{"type":"validation_error","code":"out_of_range","message":"A value is outside the schema's input bounds.","errors":[{"question_key":"body_measurements","field":"height","code":"out_of_range","message":"The value is outside the allowed range.","min":100,"max":250}],"request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-10":{"value":{"error":{"type":"validation_error","code":"upload_unusable","message":"The upload does not exist, has expired or was already used.","errors":[{"question_key":"scalp_photo_top","code":"upload_unusable","message":"The upload does not exist, has expired or was already used."}],"request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-11":{"value":{"error":{"type":"validation_error","code":"pharmacy_selection_invalid","message":"The pharmacy selection is not valid for this patient's country.","errors":[{"question_key":"pharmacy_selection","code":"pharmacy_selection_invalid","message":"Choose a pharmacy from the directory."}],"request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-12":{"value":{"error":{"type":"validation_error","code":"patient_identifier_required","message":"The patient's national identifier is required for this country.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-13":{"value":{"error":{"type":"validation_error","code":"consent_required","message":"Every consent flag must be true.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-14":{"value":{"error":{"type":"validation_error","code":"consent_notice_outdated","message":"The patient notice version is not the current one.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8","current_notice_version":"pn-2026-12"}},"summary":"Illustrative response example"},"example-15":{"value":{"error":{"type":"validation_error","code":"product_unavailable","message":"This product is not available for that country, plan or dosage.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-16":{"value":{"error":{"type":"validation_error","code":"country_conflict","message":"The answers name a country other than the product country enabled for this partner.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-17":{"value":{"error":{"type":"validation_error","code":"country_closed","message":"New consultations are not accepted for this country.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-18":{"value":{"error":{"type":"validation_error","code":"clinical_ineligibility","message":"The answers do not meet the clinical eligibility rules.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-19":{"value":{"error":{"type":"validation_error","code":"idempotency_key_reuse","message":"The Idempotency-Key was used with a different request.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request body is invalid."},"429":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"rate_limited","code":"throttled","message":"Request was throttled. Expected available in 12 seconds.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"Too many requests; wait for the number of seconds in Retry-After."}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"set -eu\numask 077\n# Path values must be URL-encoded; query string excludes the leading question mark.\nurl=\"${HD_BASE_URL:?Set API origin}/v1/partner/consultations/${HD_QUERY_STRING:+?${HD_QUERY_STRING}}\"\nprintf 'Accept: application/json\\n' > \"${HD_REQUEST_HEADERS_FILE:?Private header file}\"\nprintf 'Authorization: Bearer %s\\n' \"${HD_PARTNER_API_KEY:?Set partner API key}\" >> \"$HD_REQUEST_HEADERS_FILE\"\n# Create the Idempotency-Key once per logical request, store it before the first attempt and send the same value on every retry.\nprintf 'Idempotency-Key: %s\\n' \"${HD_IDEMPOTENCY_KEY:?Set the stored Idempotency-Key; reuse it on retry}\" >> \"$HD_REQUEST_HEADERS_FILE\"\ncurl --silent --show-error --max-time 30 --request POST \\\n  --header \"@$HD_REQUEST_HEADERS_FILE\" \\\n  --dump-header \"${HD_HEADERS_FILE:?Private response header file}\" \\\n  --output \"${HD_RESPONSE_FILE:?Private response file}\" \\\n  --write-out '%{http_code}\\n%{content_type}\\n' \\\n  --header 'Content-Type: application/json' \\\n  --data-binary \"@${HD_REQUEST_FILE:?JSON fixture file}\" \\\n  \"$url\"\n"},{"lang":"Python","label":"requests","source":"import os, json\nfrom pathlib import Path\nfrom urllib.parse import quote\nimport requests\nos.umask(0o077)\nurl = os.environ[\"HD_BASE_URL\"].rstrip(\"/\") + '/v1/partner/consultations/'\nif os.environ.get(\"HD_QUERY_STRING\"): url += \"?\" + os.environ[\"HD_QUERY_STRING\"]\nheaders = {\"Accept\": 'application/json'}\nheaders[\"Authorization\"] = \"Bearer \" + os.environ[\"HD_PARTNER_API_KEY\"]\n# Create the Idempotency-Key once per logical request, store it before the first attempt and send the same value on every retry.\nheaders[\"Idempotency-Key\"] = os.environ[\"HD_IDEMPOTENCY_KEY\"]\nkwargs = {}\nkwargs[\"json\"] = json.loads(Path(os.environ[\"HD_REQUEST_FILE\"]).read_text())\nresponse = requests.request('POST', url, headers=headers, timeout=30, allow_redirects=False, **kwargs)\nPath(os.environ[\"HD_RESPONSE_FILE\"]).write_bytes(response.content)\nPath(os.environ[\"HD_HEADERS_FILE\"]).write_text(json.dumps(dict(response.headers)))\nprint(response.status_code)\nprint(response.headers.get(\"Content-Type\", \"\"))\n"},{"lang":"JavaScript","label":"fetch","source":"import { readFileSync, writeFileSync } from \"node:fs\";\nconst env = process.env;\nlet url = env.HD_BASE_URL.replace(/\\/$/, \"\") + \"/v1/partner/consultations/\";\nif (env.HD_QUERY_STRING) url += \"?\" + env.HD_QUERY_STRING;\nconst headers = { \"Accept\": \"application/json\" };\nheaders.Authorization = \"Bearer \" + env.HD_PARTNER_API_KEY;\n// Create the Idempotency-Key once per logical request, store it before the first attempt and send the same value on every retry.\nheaders[\"Idempotency-Key\"] = env.HD_IDEMPOTENCY_KEY;\nconst options = { method: \"POST\", headers, redirect: \"manual\", signal: AbortSignal.timeout(30000) };\nheaders[\"Content-Type\"] = \"application/json\";\noptions.body = readFileSync(env.HD_REQUEST_FILE, \"utf8\");\nconst response = await fetch(url, options);\nwriteFileSync(env.HD_RESPONSE_FILE, Buffer.from(await response.arrayBuffer()), {mode: 0o600});\nwriteFileSync(env.HD_HEADERS_FILE, JSON.stringify(Object.fromEntries(response.headers)), {mode: 0o600});\nconsole.log(response.status);\nconsole.log(response.headers.get(\"content-type\") ?? \"\");\n"}]}},"/v1/partner/consultations/{consultation_id}/":{"get":{"operationId":"get_consultation","description":"The current state of one consultation: status, decision, open information request, prescriptions and billing. Requires the consultations:read scope.\n\n### Access, effects and retries\n**Ownership:** The API key's own partner and mode (test or live); another partner's or mode's records answer 404.\n\n**Prerequisites:** A partner API key with the `consultations:read` scope.\n\n**Effects:** None.\n\n**Retries:** Safe to retry.","summary":"Get a consultation by id","parameters":[{"in":"path","name":"consultation_id","schema":{"type":"string","format":"uuid"},"description":"The consultation id.","required":true,"example":"7f8b1c2e-4d5a-4e6f-9a0b-1c2d3e4f5a6b"}],"tags":["Consultations"],"security":[{"PartnerApiKey":[]}],"responses":{"200":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Consultation"},"examples":{"example-1":{"value":{"id":"7f8b1c2e-4d5a-4e6f-9a0b-1c2d3e4f5a6b","object":"consultation","mode":"test","status":"approved","version":4,"status_history":[{"status":"submitted","version":1,"at":"2026-11-03T10:15:00Z"},{"status":"in_review","version":2,"at":"2026-11-03T11:02:00Z"},{"status":"approved","version":3,"at":"2026-11-03T12:40:00Z"}],"external_reference":"order-10045","product":{"product_id":"oral_finasteride","name":"Oral finasteride"},"country":"ES","plan_key":"one_off","dosage":"1 mg","patient":{"id":"0e1d2c3b-4a59-4687-8b9a-0b1c2d3e4f50","external_patient_id":"cust-8812"},"submitted_at":"2026-11-03T10:15:00Z","updated_at":"2026-11-03T12:45:00Z","reviewer":{"description":"Hi Doctor EU-licensed doctor","licence_country":"ES"},"decision":{"outcome":"approved","reason_category":null,"decided_at":"2026-11-03T12:40:00Z"},"open_info_request":null,"prescriptions":[{"id":"5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d","consultation_id":"7f8b1c2e-4d5a-4e6f-9a0b-1c2d3e4f5a6b","status":"issued","active":true,"issued_at":"2026-11-03T12:45:00Z","cancelled_at":null,"expires_at":null,"dispensing_country":null,"medication":{"name":"oral_finasteride","prescribed_label":"oral_finasteride","dosage":"1 mg","supply_days":null},"fulfilment":{"route":"cross_border_pdf"},"collected_at":null,"replaced_by":null}],"billing":{"state":"billable","price":{"amount_cents":2900,"currency":"EUR"}},"cancellable":false},"summary":"Illustrative response example"}}}},"description":"Success."},"400":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"invalid_request","code":"brand_header_not_allowed","message":"Brand headers are not accepted on partner requests; the brand comes from the API key.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request is malformed. Partner routes also refuse the patient-app brand header with 400 brand_header_not_allowed, before authentication (no rate-limit headers)."},"401":{"headers":{"WWW-Authenticate":{"schema":{"type":"string"},"description":"The authentication scheme to use: Bearer realm=\"hi-doctor-partner\"."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"authentication_error","code":"not_authenticated","message":"Authentication credentials were not provided.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"authentication_error","code":"invalid_token","message":"The API key is not valid.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"authentication_error","code":"key_revoked","message":"The API key has been revoked.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"authentication_error","code":"key_expired","message":"The API key has expired.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"authentication_error","code":"partner_suspended","message":"The partner account is suspended.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The API key is missing, malformed, unknown, revoked or expired, or the partner account is suspended. The response carries WWW-Authenticate: Bearer realm=\"hi-doctor-partner\" and no rate-limit headers."},"403":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"permission_error","code":"insufficient_scope","message":"This endpoint requires the consultations:read scope.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"permission_error","code":"partner_not_live","message":"Live mode is not enabled for this partner.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The key lacks the consultations:read scope (insufficient_scope), or a live key was used while live mode is not enabled for the partner (partner_not_live)."},"404":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PartnerError"},{"$ref":"#/components/schemas/PartnerFlagOffError"}]},"examples":{"example-1":{"value":{"error":{"type":"not_found","code":"not_found","message":"No such resource.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"not_found"}},"summary":"Illustrative response example"}}}},"description":"No such resource for this key's partner and mode. While the Partner API is switched off for the environment, every route answers 404 with only {\"error\": {\"type\": \"not_found\"}}, before authentication."},"429":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"rate_limited","code":"throttled","message":"Request was throttled. Expected available in 12 seconds.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"Too many requests; wait for the number of seconds in Retry-After."}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"set -eu\numask 077\n# Path values must be URL-encoded; query string excludes the leading question mark.\nurl=\"${HD_BASE_URL:?Set API origin}/v1/partner/consultations/${HD_PATH_CONSULTATION_ID:?Set the path fixture}/${HD_QUERY_STRING:+?${HD_QUERY_STRING}}\"\nprintf 'Accept: application/json\\n' > \"${HD_REQUEST_HEADERS_FILE:?Private header file}\"\nprintf 'Authorization: Bearer %s\\n' \"${HD_PARTNER_API_KEY:?Set partner API key}\" >> \"$HD_REQUEST_HEADERS_FILE\"\ncurl --silent --show-error --max-time 30 --request GET \\\n  --header \"@$HD_REQUEST_HEADERS_FILE\" \\\n  --dump-header \"${HD_HEADERS_FILE:?Private response header file}\" \\\n  --output \"${HD_RESPONSE_FILE:?Private response file}\" \\\n  --write-out '%{http_code}\\n%{content_type}\\n' \\\n  \"$url\"\n"},{"lang":"Python","label":"requests","source":"import os, json\nfrom pathlib import Path\nfrom urllib.parse import quote\nimport requests\nos.umask(0o077)\nurl = os.environ[\"HD_BASE_URL\"].rstrip(\"/\") + '/v1/partner/consultations/{consultation_id}/'\nurl = url.replace('{consultation_id}', quote(os.environ['HD_PATH_CONSULTATION_ID'], safe=\"\"))\nif os.environ.get(\"HD_QUERY_STRING\"): url += \"?\" + os.environ[\"HD_QUERY_STRING\"]\nheaders = {\"Accept\": 'application/json'}\nheaders[\"Authorization\"] = \"Bearer \" + os.environ[\"HD_PARTNER_API_KEY\"]\nkwargs = {}\nresponse = requests.request('GET', url, headers=headers, timeout=30, allow_redirects=False, **kwargs)\nPath(os.environ[\"HD_RESPONSE_FILE\"]).write_bytes(response.content)\nPath(os.environ[\"HD_HEADERS_FILE\"]).write_text(json.dumps(dict(response.headers)))\nprint(response.status_code)\nprint(response.headers.get(\"Content-Type\", \"\"))\n"},{"lang":"JavaScript","label":"fetch","source":"import { readFileSync, writeFileSync } from \"node:fs\";\nconst env = process.env;\nlet url = env.HD_BASE_URL.replace(/\\/$/, \"\") + \"/v1/partner/consultations/{consultation_id}/\";\nurl = url.replace(\"{consultation_id}\", encodeURIComponent(env.HD_PATH_CONSULTATION_ID));\nif (env.HD_QUERY_STRING) url += \"?\" + env.HD_QUERY_STRING;\nconst headers = { \"Accept\": \"application/json\" };\nheaders.Authorization = \"Bearer \" + env.HD_PARTNER_API_KEY;\nconst options = { method: \"GET\", headers, redirect: \"manual\", signal: AbortSignal.timeout(30000) };\nconst response = await fetch(url, options);\nwriteFileSync(env.HD_RESPONSE_FILE, Buffer.from(await response.arrayBuffer()), {mode: 0o600});\nwriteFileSync(env.HD_HEADERS_FILE, JSON.stringify(Object.fromEntries(response.headers)), {mode: 0o600});\nconsole.log(response.status);\nconsole.log(response.headers.get(\"content-type\") ?? \"\");\n"}]}},"/v1/partner/consultations/{consultation_id}/cancel/":{"post":{"operationId":"cancel_consultation","description":"Cancels a consultation that is still submitted; check cancellable first, which reports exactly this rule. It is refused with 409 not_cancellable once the reviewing doctor has opened the consultation or it has a decision or a prescription, and also while it is unopened but clinically locked (a decision was recorded or an electronic prescription signing has started) or a prescription issuance or replacement is in flight. 409 consultation_busy means the clinical record is locked right now; retry shortly. A cancelled consultation is not billed. Requires the consultations:write scope.\n\n### Access, effects and retries\n**Ownership:** The API key's own partner and mode (test or live); another partner's or mode's records answer 404.\n\n**Prerequisites:** A partner API key with the `consultations:write` scope.\n\n**Effects:** Cancels the consultation and voids its pending billing; emits a consultation webhook event.\n\n**Retries:** A retry after a successful cancel answers 409 not_cancellable; read the consultation to confirm.","summary":"Cancel a consultation","parameters":[{"in":"path","name":"consultation_id","schema":{"type":"string","format":"uuid"},"description":"The consultation id.","required":true,"example":"7f8b1c2e-4d5a-4e6f-9a0b-1c2d3e4f5a6b"},{"in":"header","name":"Idempotency-Key","schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{8,128}$"},"description":"Accepted but not used for replay on this operation. Only POST /v1/partner/consultations stores and replays responses by Idempotency-Key.","example":"b7c1d2e3f4a5b6c7"}],"tags":["Consultations"],"security":[{"PartnerApiKey":[]}],"responses":{"200":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Consultation"},"examples":{"example-1":{"value":{"id":"7f8b1c2e-4d5a-4e6f-9a0b-1c2d3e4f5a6b","object":"consultation","mode":"test","status":"cancelled","version":2,"status_history":[{"status":"submitted","version":1,"at":"2026-11-03T10:15:00Z"},{"status":"cancelled","version":2,"at":"2026-11-03T12:40:00Z"}],"external_reference":"order-10045","product":{"product_id":"oral_finasteride","name":"Oral finasteride"},"country":"ES","plan_key":"one_off","dosage":"1 mg","patient":{"id":"0e1d2c3b-4a59-4687-8b9a-0b1c2d3e4f50","external_patient_id":"cust-8812"},"submitted_at":"2026-11-03T10:15:00Z","updated_at":"2026-11-03T12:40:00Z","reviewer":null,"decision":null,"open_info_request":null,"prescriptions":[],"billing":{"state":"void","price":{"amount_cents":2900,"currency":"EUR"}},"cancellable":false},"summary":"Illustrative response example"}}}},"description":"The cancelled consultation."},"400":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"invalid_request","code":"brand_header_not_allowed","message":"Brand headers are not accepted on partner requests; the brand comes from the API key.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request is malformed. Partner routes also refuse the patient-app brand header with 400 brand_header_not_allowed, before authentication (no rate-limit headers)."},"401":{"headers":{"WWW-Authenticate":{"schema":{"type":"string"},"description":"The authentication scheme to use: Bearer realm=\"hi-doctor-partner\"."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"authentication_error","code":"not_authenticated","message":"Authentication credentials were not provided.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"authentication_error","code":"invalid_token","message":"The API key is not valid.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"authentication_error","code":"key_revoked","message":"The API key has been revoked.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"authentication_error","code":"key_expired","message":"The API key has expired.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"authentication_error","code":"partner_suspended","message":"The partner account is suspended.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The API key is missing, malformed, unknown, revoked or expired, or the partner account is suspended. The response carries WWW-Authenticate: Bearer realm=\"hi-doctor-partner\" and no rate-limit headers."},"403":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"permission_error","code":"insufficient_scope","message":"This endpoint requires the consultations:write scope.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"permission_error","code":"partner_not_live","message":"Live mode is not enabled for this partner.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The key lacks the consultations:write scope (insufficient_scope), or a live key was used while live mode is not enabled for the partner (partner_not_live)."},"404":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PartnerError"},{"$ref":"#/components/schemas/PartnerFlagOffError"}]},"examples":{"example-1":{"value":{"error":{"type":"not_found","code":"not_found","message":"No such resource.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"not_found"}},"summary":"Illustrative response example"}}}},"description":"No such resource for this key's partner and mode. While the Partner API is switched off for the environment, every route answers 404 with only {\"error\": {\"type\": \"not_found\"}}, before authentication."},"409":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"conflict","code":"not_cancellable","message":"The consultation has been opened by the reviewing doctor or decided.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"conflict","code":"consultation_busy","message":"The consultation is busy; retry shortly.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request conflicts with the resource's current state."},"429":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"rate_limited","code":"throttled","message":"Request was throttled. Expected available in 12 seconds.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"Too many requests; wait for the number of seconds in Retry-After."}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"set -eu\numask 077\n# Path values must be URL-encoded; query string excludes the leading question mark.\nurl=\"${HD_BASE_URL:?Set API origin}/v1/partner/consultations/${HD_PATH_CONSULTATION_ID:?Set the path fixture}/cancel/${HD_QUERY_STRING:+?${HD_QUERY_STRING}}\"\nprintf 'Accept: application/json\\n' > \"${HD_REQUEST_HEADERS_FILE:?Private header file}\"\nprintf 'Authorization: Bearer %s\\n' \"${HD_PARTNER_API_KEY:?Set partner API key}\" >> \"$HD_REQUEST_HEADERS_FILE\"\ncurl --silent --show-error --max-time 30 --request POST \\\n  --header \"@$HD_REQUEST_HEADERS_FILE\" \\\n  --dump-header \"${HD_HEADERS_FILE:?Private response header file}\" \\\n  --output \"${HD_RESPONSE_FILE:?Private response file}\" \\\n  --write-out '%{http_code}\\n%{content_type}\\n' \\\n  \"$url\"\n"},{"lang":"Python","label":"requests","source":"import os, json\nfrom pathlib import Path\nfrom urllib.parse import quote\nimport requests\nos.umask(0o077)\nurl = os.environ[\"HD_BASE_URL\"].rstrip(\"/\") + '/v1/partner/consultations/{consultation_id}/cancel/'\nurl = url.replace('{consultation_id}', quote(os.environ['HD_PATH_CONSULTATION_ID'], safe=\"\"))\nif os.environ.get(\"HD_QUERY_STRING\"): url += \"?\" + os.environ[\"HD_QUERY_STRING\"]\nheaders = {\"Accept\": 'application/json'}\nheaders[\"Authorization\"] = \"Bearer \" + os.environ[\"HD_PARTNER_API_KEY\"]\nkwargs = {}\nresponse = requests.request('POST', url, headers=headers, timeout=30, allow_redirects=False, **kwargs)\nPath(os.environ[\"HD_RESPONSE_FILE\"]).write_bytes(response.content)\nPath(os.environ[\"HD_HEADERS_FILE\"]).write_text(json.dumps(dict(response.headers)))\nprint(response.status_code)\nprint(response.headers.get(\"Content-Type\", \"\"))\n"},{"lang":"JavaScript","label":"fetch","source":"import { readFileSync, writeFileSync } from \"node:fs\";\nconst env = process.env;\nlet url = env.HD_BASE_URL.replace(/\\/$/, \"\") + \"/v1/partner/consultations/{consultation_id}/cancel/\";\nurl = url.replace(\"{consultation_id}\", encodeURIComponent(env.HD_PATH_CONSULTATION_ID));\nif (env.HD_QUERY_STRING) url += \"?\" + env.HD_QUERY_STRING;\nconst headers = { \"Accept\": \"application/json\" };\nheaders.Authorization = \"Bearer \" + env.HD_PARTNER_API_KEY;\nconst options = { method: \"POST\", headers, redirect: \"manual\", signal: AbortSignal.timeout(30000) };\nconst response = await fetch(url, options);\nwriteFileSync(env.HD_RESPONSE_FILE, Buffer.from(await response.arrayBuffer()), {mode: 0o600});\nwriteFileSync(env.HD_HEADERS_FILE, JSON.stringify(Object.fromEntries(response.headers)), {mode: 0o600});\nconsole.log(response.status);\nconsole.log(response.headers.get(\"content-type\") ?? \"\");\n"}]}},"/v1/partner/patients/":{"get":{"operationId":"list_patients","description":"Patients you have submitted consultations for, in this key's mode, newest first. Requires the patients:read scope.\n\n### Access, effects and retries\n**Ownership:** The API key's own partner and mode (test or live).\n\n**Prerequisites:** A partner API key with the `patients:read` scope.\n\n**Effects:** None.\n\n**Retries:** Safe to retry.","summary":"List patients","parameters":[{"in":"query","name":"external_patient_id","schema":{"type":"string"},"description":"Only the patient with this external_patient_id from your system.","example":"cust-8812"},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Page size, 1 to 100. Default 50. A value outside the range is refused with 400 invalid_request.","example":50},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"The next_cursor of the previous page.","example":"MjAyNi0xMS0wM1QxMDoxNTowMCswMDowMHw3ZjhiMWMyZS00ZDVhLTRlNmYtOWEwYi0xYzJkM2U0ZjVhNmI="}],"tags":["Patients"],"security":[{"PartnerApiKey":[]}],"responses":{"200":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerPatientList"},"examples":{"example-1":{"value":{"object":"list","results":[{"id":"0e1d2c3b-4a59-4687-8b9a-0b1c2d3e4f50","object":"patient","mode":"test","external_patient_id":"cust-8812","created_at":"2026-11-03T10:15:00Z","consent":{"notice_version":"pn-2026-11","accepted_at":"2026-11-03T10:10:00Z","withdrawn_at":null},"consultation_count":1}],"next_cursor":null},"summary":"Illustrative response example"}}}},"description":"Success."},"400":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"invalid_request","code":"invalid_request","message":"limit must be between 1 and 100.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"invalid_request","code":"invalid_cursor","message":"The pagination cursor is not valid.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"invalid_request","code":"brand_header_not_allowed","message":"Brand headers are not accepted on partner requests; the brand comes from the API key.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request is malformed. Partner routes also refuse the patient-app brand header with 400 brand_header_not_allowed, before authentication (no rate-limit headers)."},"401":{"headers":{"WWW-Authenticate":{"schema":{"type":"string"},"description":"The authentication scheme to use: Bearer realm=\"hi-doctor-partner\"."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"authentication_error","code":"not_authenticated","message":"Authentication credentials were not provided.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"authentication_error","code":"invalid_token","message":"The API key is not valid.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"authentication_error","code":"key_revoked","message":"The API key has been revoked.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"authentication_error","code":"key_expired","message":"The API key has expired.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"authentication_error","code":"partner_suspended","message":"The partner account is suspended.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The API key is missing, malformed, unknown, revoked or expired, or the partner account is suspended. The response carries WWW-Authenticate: Bearer realm=\"hi-doctor-partner\" and no rate-limit headers."},"403":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"permission_error","code":"insufficient_scope","message":"This endpoint requires the patients:read scope.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"permission_error","code":"partner_not_live","message":"Live mode is not enabled for this partner.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The key lacks the patients:read scope (insufficient_scope), or a live key was used while live mode is not enabled for the partner (partner_not_live)."},"404":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PartnerError"},{"$ref":"#/components/schemas/PartnerFlagOffError"}]},"examples":{"example-1":{"value":{"error":{"type":"not_found"}},"summary":"Illustrative response example"}}}},"description":"While the Partner API is switched off for the environment, every route answers 404 with only {\"error\": {\"type\": \"not_found\"}}, before authentication."},"429":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"rate_limited","code":"throttled","message":"Request was throttled. Expected available in 12 seconds.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"Too many requests; wait for the number of seconds in Retry-After."}},"x-pagination":{"kind":"cursor","defaultLimit":50,"maxLimit":100,"ordering":["-created_at"],"defaultOrdering":"-created_at"},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"set -eu\numask 077\n# Path values must be URL-encoded; query string excludes the leading question mark.\nurl=\"${HD_BASE_URL:?Set API origin}/v1/partner/patients/${HD_QUERY_STRING:+?${HD_QUERY_STRING}}\"\nprintf 'Accept: application/json\\n' > \"${HD_REQUEST_HEADERS_FILE:?Private header file}\"\nprintf 'Authorization: Bearer %s\\n' \"${HD_PARTNER_API_KEY:?Set partner API key}\" >> \"$HD_REQUEST_HEADERS_FILE\"\ncurl --silent --show-error --max-time 30 --request GET \\\n  --header \"@$HD_REQUEST_HEADERS_FILE\" \\\n  --dump-header \"${HD_HEADERS_FILE:?Private response header file}\" \\\n  --output \"${HD_RESPONSE_FILE:?Private response file}\" \\\n  --write-out '%{http_code}\\n%{content_type}\\n' \\\n  \"$url\"\n"},{"lang":"Python","label":"requests","source":"import os, json\nfrom pathlib import Path\nfrom urllib.parse import quote\nimport requests\nos.umask(0o077)\nurl = os.environ[\"HD_BASE_URL\"].rstrip(\"/\") + '/v1/partner/patients/'\nif os.environ.get(\"HD_QUERY_STRING\"): url += \"?\" + os.environ[\"HD_QUERY_STRING\"]\nheaders = {\"Accept\": 'application/json'}\nheaders[\"Authorization\"] = \"Bearer \" + os.environ[\"HD_PARTNER_API_KEY\"]\nkwargs = {}\nresponse = requests.request('GET', url, headers=headers, timeout=30, allow_redirects=False, **kwargs)\nPath(os.environ[\"HD_RESPONSE_FILE\"]).write_bytes(response.content)\nPath(os.environ[\"HD_HEADERS_FILE\"]).write_text(json.dumps(dict(response.headers)))\nprint(response.status_code)\nprint(response.headers.get(\"Content-Type\", \"\"))\n"},{"lang":"JavaScript","label":"fetch","source":"import { readFileSync, writeFileSync } from \"node:fs\";\nconst env = process.env;\nlet url = env.HD_BASE_URL.replace(/\\/$/, \"\") + \"/v1/partner/patients/\";\nif (env.HD_QUERY_STRING) url += \"?\" + env.HD_QUERY_STRING;\nconst headers = { \"Accept\": \"application/json\" };\nheaders.Authorization = \"Bearer \" + env.HD_PARTNER_API_KEY;\nconst options = { method: \"GET\", headers, redirect: \"manual\", signal: AbortSignal.timeout(30000) };\nconst response = await fetch(url, options);\nwriteFileSync(env.HD_RESPONSE_FILE, Buffer.from(await response.arrayBuffer()), {mode: 0o600});\nwriteFileSync(env.HD_HEADERS_FILE, JSON.stringify(Object.fromEntries(response.headers)), {mode: 0o600});\nconsole.log(response.status);\nconsole.log(response.headers.get(\"content-type\") ?? \"\");\n"}]}},"/v1/partner/patients/{patient_id}/":{"get":{"operationId":"get_patient","description":"One patient and their consent record. Requires the patients:read scope.\n\n### Access, effects and retries\n**Ownership:** The API key's own partner and mode (test or live); another partner's or mode's records answer 404.\n\n**Prerequisites:** A partner API key with the `patients:read` scope.\n\n**Effects:** None.\n\n**Retries:** Safe to retry.","summary":"Get a patient","parameters":[{"in":"path","name":"patient_id","schema":{"type":"string","format":"uuid"},"description":"The patient id.","required":true,"example":"0e1d2c3b-4a59-4687-8b9a-0b1c2d3e4f50"}],"tags":["Patients"],"security":[{"PartnerApiKey":[]}],"responses":{"200":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerPatient"},"examples":{"example-1":{"value":{"id":"0e1d2c3b-4a59-4687-8b9a-0b1c2d3e4f50","object":"patient","mode":"test","external_patient_id":"cust-8812","created_at":"2026-11-03T10:15:00Z","consent":{"notice_version":"pn-2026-11","accepted_at":"2026-11-03T10:10:00Z","withdrawn_at":null},"consultation_count":1},"summary":"Illustrative response example"}}}},"description":"Success."},"400":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"invalid_request","code":"brand_header_not_allowed","message":"Brand headers are not accepted on partner requests; the brand comes from the API key.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request is malformed. Partner routes also refuse the patient-app brand header with 400 brand_header_not_allowed, before authentication (no rate-limit headers)."},"401":{"headers":{"WWW-Authenticate":{"schema":{"type":"string"},"description":"The authentication scheme to use: Bearer realm=\"hi-doctor-partner\"."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"authentication_error","code":"not_authenticated","message":"Authentication credentials were not provided.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"authentication_error","code":"invalid_token","message":"The API key is not valid.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"authentication_error","code":"key_revoked","message":"The API key has been revoked.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"authentication_error","code":"key_expired","message":"The API key has expired.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"authentication_error","code":"partner_suspended","message":"The partner account is suspended.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The API key is missing, malformed, unknown, revoked or expired, or the partner account is suspended. The response carries WWW-Authenticate: Bearer realm=\"hi-doctor-partner\" and no rate-limit headers."},"403":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"permission_error","code":"insufficient_scope","message":"This endpoint requires the patients:read scope.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"permission_error","code":"partner_not_live","message":"Live mode is not enabled for this partner.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The key lacks the patients:read scope (insufficient_scope), or a live key was used while live mode is not enabled for the partner (partner_not_live)."},"404":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PartnerError"},{"$ref":"#/components/schemas/PartnerFlagOffError"}]},"examples":{"example-1":{"value":{"error":{"type":"not_found","code":"not_found","message":"No such resource.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"not_found"}},"summary":"Illustrative response example"}}}},"description":"No such resource for this key's partner and mode. While the Partner API is switched off for the environment, every route answers 404 with only {\"error\": {\"type\": \"not_found\"}}, before authentication."},"429":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"rate_limited","code":"throttled","message":"Request was throttled. Expected available in 12 seconds.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"Too many requests; wait for the number of seconds in Retry-After."}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"set -eu\numask 077\n# Path values must be URL-encoded; query string excludes the leading question mark.\nurl=\"${HD_BASE_URL:?Set API origin}/v1/partner/patients/${HD_PATH_PATIENT_ID:?Set the path fixture}/${HD_QUERY_STRING:+?${HD_QUERY_STRING}}\"\nprintf 'Accept: application/json\\n' > \"${HD_REQUEST_HEADERS_FILE:?Private header file}\"\nprintf 'Authorization: Bearer %s\\n' \"${HD_PARTNER_API_KEY:?Set partner API key}\" >> \"$HD_REQUEST_HEADERS_FILE\"\ncurl --silent --show-error --max-time 30 --request GET \\\n  --header \"@$HD_REQUEST_HEADERS_FILE\" \\\n  --dump-header \"${HD_HEADERS_FILE:?Private response header file}\" \\\n  --output \"${HD_RESPONSE_FILE:?Private response file}\" \\\n  --write-out '%{http_code}\\n%{content_type}\\n' \\\n  \"$url\"\n"},{"lang":"Python","label":"requests","source":"import os, json\nfrom pathlib import Path\nfrom urllib.parse import quote\nimport requests\nos.umask(0o077)\nurl = os.environ[\"HD_BASE_URL\"].rstrip(\"/\") + '/v1/partner/patients/{patient_id}/'\nurl = url.replace('{patient_id}', quote(os.environ['HD_PATH_PATIENT_ID'], safe=\"\"))\nif os.environ.get(\"HD_QUERY_STRING\"): url += \"?\" + os.environ[\"HD_QUERY_STRING\"]\nheaders = {\"Accept\": 'application/json'}\nheaders[\"Authorization\"] = \"Bearer \" + os.environ[\"HD_PARTNER_API_KEY\"]\nkwargs = {}\nresponse = requests.request('GET', url, headers=headers, timeout=30, allow_redirects=False, **kwargs)\nPath(os.environ[\"HD_RESPONSE_FILE\"]).write_bytes(response.content)\nPath(os.environ[\"HD_HEADERS_FILE\"]).write_text(json.dumps(dict(response.headers)))\nprint(response.status_code)\nprint(response.headers.get(\"Content-Type\", \"\"))\n"},{"lang":"JavaScript","label":"fetch","source":"import { readFileSync, writeFileSync } from \"node:fs\";\nconst env = process.env;\nlet url = env.HD_BASE_URL.replace(/\\/$/, \"\") + \"/v1/partner/patients/{patient_id}/\";\nurl = url.replace(\"{patient_id}\", encodeURIComponent(env.HD_PATH_PATIENT_ID));\nif (env.HD_QUERY_STRING) url += \"?\" + env.HD_QUERY_STRING;\nconst headers = { \"Accept\": \"application/json\" };\nheaders.Authorization = \"Bearer \" + env.HD_PARTNER_API_KEY;\nconst options = { method: \"GET\", headers, redirect: \"manual\", signal: AbortSignal.timeout(30000) };\nconst response = await fetch(url, options);\nwriteFileSync(env.HD_RESPONSE_FILE, Buffer.from(await response.arrayBuffer()), {mode: 0o600});\nwriteFileSync(env.HD_HEADERS_FILE, JSON.stringify(Object.fromEntries(response.headers)), {mode: 0o600});\nconsole.log(response.status);\nconsole.log(response.headers.get(\"content-type\") ?? \"\");\n"}]}},"/v1/partner/patients/{patient_id}/consent-withdrawal/":{"post":{"operationId":"withdraw_patient_consent","description":"Records that the patient withdrew consent and withdraws their open information requests. Later submissions for the patient are refused with 409 consent_withdrawn. Answering a request this withdrew is refused with 409 info_request_not_open, because the request's status is checked first; only a request still open for the patient (raised after the withdrawal) is refused with 409 consent_withdrawn. In live mode Hi Doctor's medical team is notified and handles any open consultation; open consultations are not cancelled or frozen automatically, and a test consultation can still be advanced. Repeating the call keeps the first withdrawal time. Requires the consultations:write and patients:read scopes.\n\n### Access, effects and retries\n**Ownership:** The API key's own partner and mode (test or live); another partner's or mode's records answer 404.\n\n**Prerequisites:** A partner API key with the `patients:read` and `consultations:write` scopes.\n\n**Effects:** Records the withdrawal and withdraws the patient's open information requests; in live mode notifies Hi Doctor's medical team. Open consultations are not cancelled or frozen automatically, and a test consultation can still be advanced.\n\n**Retries:** Safe to retry: a repeat keeps the first withdrawal time.","summary":"Record a consent withdrawal","parameters":[{"in":"path","name":"patient_id","schema":{"type":"string","format":"uuid"},"description":"The patient id.","required":true,"example":"0e1d2c3b-4a59-4687-8b9a-0b1c2d3e4f50"},{"in":"header","name":"Idempotency-Key","schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{8,128}$"},"description":"Accepted but not used for replay on this operation. Only POST /v1/partner/consultations stores and replays responses by Idempotency-Key.","example":"b7c1d2e3f4a5b6c7"}],"tags":["Patients"],"security":[{"PartnerApiKey":[]}],"responses":{"200":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerPatient"},"examples":{"example-1":{"value":{"id":"0e1d2c3b-4a59-4687-8b9a-0b1c2d3e4f50","object":"patient","mode":"test","external_patient_id":"cust-8812","created_at":"2026-11-03T10:15:00Z","consent":{"notice_version":"pn-2026-11","accepted_at":"2026-11-03T10:10:00Z","withdrawn_at":"2026-11-03T12:40:00Z"},"consultation_count":1},"summary":"Illustrative response example"}}}},"description":"The patient with the withdrawal recorded."},"400":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"invalid_request","code":"brand_header_not_allowed","message":"Brand headers are not accepted on partner requests; the brand comes from the API key.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request is malformed. Partner routes also refuse the patient-app brand header with 400 brand_header_not_allowed, before authentication (no rate-limit headers)."},"401":{"headers":{"WWW-Authenticate":{"schema":{"type":"string"},"description":"The authentication scheme to use: Bearer realm=\"hi-doctor-partner\"."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"authentication_error","code":"not_authenticated","message":"Authentication credentials were not provided.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"authentication_error","code":"invalid_token","message":"The API key is not valid.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"authentication_error","code":"key_revoked","message":"The API key has been revoked.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"authentication_error","code":"key_expired","message":"The API key has expired.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"authentication_error","code":"partner_suspended","message":"The partner account is suspended.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The API key is missing, malformed, unknown, revoked or expired, or the partner account is suspended. The response carries WWW-Authenticate: Bearer realm=\"hi-doctor-partner\" and no rate-limit headers."},"403":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"permission_error","code":"insufficient_scope","message":"This endpoint requires the consultations:write, patients:read scope.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"permission_error","code":"partner_not_live","message":"Live mode is not enabled for this partner.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The key lacks the consultations:write, patients:read scopes (insufficient_scope), or a live key was used while live mode is not enabled for the partner (partner_not_live)."},"404":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PartnerError"},{"$ref":"#/components/schemas/PartnerFlagOffError"}]},"examples":{"example-1":{"value":{"error":{"type":"not_found","code":"not_found","message":"No such resource.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"not_found"}},"summary":"Illustrative response example"}}}},"description":"No such resource for this key's partner and mode. While the Partner API is switched off for the environment, every route answers 404 with only {\"error\": {\"type\": \"not_found\"}}, before authentication."},"429":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"rate_limited","code":"throttled","message":"Request was throttled. Expected available in 12 seconds.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"Too many requests; wait for the number of seconds in Retry-After."}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"set -eu\numask 077\n# Path values must be URL-encoded; query string excludes the leading question mark.\nurl=\"${HD_BASE_URL:?Set API origin}/v1/partner/patients/${HD_PATH_PATIENT_ID:?Set the path fixture}/consent-withdrawal/${HD_QUERY_STRING:+?${HD_QUERY_STRING}}\"\nprintf 'Accept: application/json\\n' > \"${HD_REQUEST_HEADERS_FILE:?Private header file}\"\nprintf 'Authorization: Bearer %s\\n' \"${HD_PARTNER_API_KEY:?Set partner API key}\" >> \"$HD_REQUEST_HEADERS_FILE\"\ncurl --silent --show-error --max-time 30 --request POST \\\n  --header \"@$HD_REQUEST_HEADERS_FILE\" \\\n  --dump-header \"${HD_HEADERS_FILE:?Private response header file}\" \\\n  --output \"${HD_RESPONSE_FILE:?Private response file}\" \\\n  --write-out '%{http_code}\\n%{content_type}\\n' \\\n  \"$url\"\n"},{"lang":"Python","label":"requests","source":"import os, json\nfrom pathlib import Path\nfrom urllib.parse import quote\nimport requests\nos.umask(0o077)\nurl = os.environ[\"HD_BASE_URL\"].rstrip(\"/\") + '/v1/partner/patients/{patient_id}/consent-withdrawal/'\nurl = url.replace('{patient_id}', quote(os.environ['HD_PATH_PATIENT_ID'], safe=\"\"))\nif os.environ.get(\"HD_QUERY_STRING\"): url += \"?\" + os.environ[\"HD_QUERY_STRING\"]\nheaders = {\"Accept\": 'application/json'}\nheaders[\"Authorization\"] = \"Bearer \" + os.environ[\"HD_PARTNER_API_KEY\"]\nkwargs = {}\nresponse = requests.request('POST', url, headers=headers, timeout=30, allow_redirects=False, **kwargs)\nPath(os.environ[\"HD_RESPONSE_FILE\"]).write_bytes(response.content)\nPath(os.environ[\"HD_HEADERS_FILE\"]).write_text(json.dumps(dict(response.headers)))\nprint(response.status_code)\nprint(response.headers.get(\"Content-Type\", \"\"))\n"},{"lang":"JavaScript","label":"fetch","source":"import { readFileSync, writeFileSync } from \"node:fs\";\nconst env = process.env;\nlet url = env.HD_BASE_URL.replace(/\\/$/, \"\") + \"/v1/partner/patients/{patient_id}/consent-withdrawal/\";\nurl = url.replace(\"{patient_id}\", encodeURIComponent(env.HD_PATH_PATIENT_ID));\nif (env.HD_QUERY_STRING) url += \"?\" + env.HD_QUERY_STRING;\nconst headers = { \"Accept\": \"application/json\" };\nheaders.Authorization = \"Bearer \" + env.HD_PARTNER_API_KEY;\nconst options = { method: \"POST\", headers, redirect: \"manual\", signal: AbortSignal.timeout(30000) };\nconst response = await fetch(url, options);\nwriteFileSync(env.HD_RESPONSE_FILE, Buffer.from(await response.arrayBuffer()), {mode: 0o600});\nwriteFileSync(env.HD_HEADERS_FILE, JSON.stringify(Object.fromEntries(response.headers)), {mode: 0o600});\nconsole.log(response.status);\nconsole.log(response.headers.get(\"content-type\") ?? \"\");\n"}]}},"/v1/partner/consultations/{consultation_id}/info-requests/":{"get":{"operationId":"list_consultation_info_requests","description":"Every information request the reviewing doctor raised on this consultation, newest first. Each question's label is written in the locale parameter's language (else the partner's default locale), never as a message key. Requires the consultations:read scope.\n\n### Access, effects and retries\n**Ownership:** The API key's own partner and mode (test or live); another partner's or mode's records answer 404.\n\n**Prerequisites:** A partner API key with the `consultations:read` scope.\n\n**Effects:** None.\n\n**Retries:** Safe to retry.","summary":"List a consultation's information requests","parameters":[{"in":"path","name":"consultation_id","schema":{"type":"string","format":"uuid"},"description":"The consultation id.","required":true,"example":"7f8b1c2e-4d5a-4e6f-9a0b-1c2d3e4f5a6b"},{"in":"query","name":"locale","schema":{"type":"string"},"description":"Language for the question labels: one of en, es, zh. Defaults to the partner's default locale; any other value falls back to English.","example":"en"}],"tags":["Information requests"],"security":[{"PartnerApiKey":[]}],"responses":{"200":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InfoRequestList"},"examples":{"example-1":{"value":{"object":"list","results":[{"id":"9c8b7a69-5847-4362-9150-4f3e2d1c0b0a","object":"info_request","consultation_id":"7f8b1c2e-4d5a-4e6f-9a0b-1c2d3e4f5a6b","status":"open","reason_code":"photo_quality","reason_label":"A photo is not clear enough for a clinical review.","question_keys":["scalp_photo_top"],"questions":[{"key":"scalp_photo_top","label":"Photo of the top of your head","type":"upload"}],"documents_requested":[],"requested_at":"2026-11-03T11:20:00Z","answered_at":null,"withdrawn_at":null,"answered_question_keys":[],"answer_count":0,"answer_document_count":0}]},"summary":"Illustrative response example"}}}},"description":"Success."},"400":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"invalid_request","code":"brand_header_not_allowed","message":"Brand headers are not accepted on partner requests; the brand comes from the API key.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request is malformed. Partner routes also refuse the patient-app brand header with 400 brand_header_not_allowed, before authentication (no rate-limit headers)."},"401":{"headers":{"WWW-Authenticate":{"schema":{"type":"string"},"description":"The authentication scheme to use: Bearer realm=\"hi-doctor-partner\"."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"authentication_error","code":"not_authenticated","message":"Authentication credentials were not provided.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"authentication_error","code":"invalid_token","message":"The API key is not valid.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"authentication_error","code":"key_revoked","message":"The API key has been revoked.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"authentication_error","code":"key_expired","message":"The API key has expired.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"authentication_error","code":"partner_suspended","message":"The partner account is suspended.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The API key is missing, malformed, unknown, revoked or expired, or the partner account is suspended. The response carries WWW-Authenticate: Bearer realm=\"hi-doctor-partner\" and no rate-limit headers."},"403":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"permission_error","code":"insufficient_scope","message":"This endpoint requires the consultations:read scope.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"permission_error","code":"partner_not_live","message":"Live mode is not enabled for this partner.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The key lacks the consultations:read scope (insufficient_scope), or a live key was used while live mode is not enabled for the partner (partner_not_live)."},"404":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PartnerError"},{"$ref":"#/components/schemas/PartnerFlagOffError"}]},"examples":{"example-1":{"value":{"error":{"type":"not_found","code":"not_found","message":"No such resource.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"not_found"}},"summary":"Illustrative response example"}}}},"description":"No such resource for this key's partner and mode. While the Partner API is switched off for the environment, every route answers 404 with only {\"error\": {\"type\": \"not_found\"}}, before authentication."},"429":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"rate_limited","code":"throttled","message":"Request was throttled. Expected available in 12 seconds.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"Too many requests; wait for the number of seconds in Retry-After."}},"x-pagination":{"kind":"unpaginated_list_envelope"},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"set -eu\numask 077\n# Path values must be URL-encoded; query string excludes the leading question mark.\nurl=\"${HD_BASE_URL:?Set API origin}/v1/partner/consultations/${HD_PATH_CONSULTATION_ID:?Set the path fixture}/info-requests/${HD_QUERY_STRING:+?${HD_QUERY_STRING}}\"\nprintf 'Accept: application/json\\n' > \"${HD_REQUEST_HEADERS_FILE:?Private header file}\"\nprintf 'Authorization: Bearer %s\\n' \"${HD_PARTNER_API_KEY:?Set partner API key}\" >> \"$HD_REQUEST_HEADERS_FILE\"\ncurl --silent --show-error --max-time 30 --request GET \\\n  --header \"@$HD_REQUEST_HEADERS_FILE\" \\\n  --dump-header \"${HD_HEADERS_FILE:?Private response header file}\" \\\n  --output \"${HD_RESPONSE_FILE:?Private response file}\" \\\n  --write-out '%{http_code}\\n%{content_type}\\n' \\\n  \"$url\"\n"},{"lang":"Python","label":"requests","source":"import os, json\nfrom pathlib import Path\nfrom urllib.parse import quote\nimport requests\nos.umask(0o077)\nurl = os.environ[\"HD_BASE_URL\"].rstrip(\"/\") + '/v1/partner/consultations/{consultation_id}/info-requests/'\nurl = url.replace('{consultation_id}', quote(os.environ['HD_PATH_CONSULTATION_ID'], safe=\"\"))\nif os.environ.get(\"HD_QUERY_STRING\"): url += \"?\" + os.environ[\"HD_QUERY_STRING\"]\nheaders = {\"Accept\": 'application/json'}\nheaders[\"Authorization\"] = \"Bearer \" + os.environ[\"HD_PARTNER_API_KEY\"]\nkwargs = {}\nresponse = requests.request('GET', url, headers=headers, timeout=30, allow_redirects=False, **kwargs)\nPath(os.environ[\"HD_RESPONSE_FILE\"]).write_bytes(response.content)\nPath(os.environ[\"HD_HEADERS_FILE\"]).write_text(json.dumps(dict(response.headers)))\nprint(response.status_code)\nprint(response.headers.get(\"Content-Type\", \"\"))\n"},{"lang":"JavaScript","label":"fetch","source":"import { readFileSync, writeFileSync } from \"node:fs\";\nconst env = process.env;\nlet url = env.HD_BASE_URL.replace(/\\/$/, \"\") + \"/v1/partner/consultations/{consultation_id}/info-requests/\";\nurl = url.replace(\"{consultation_id}\", encodeURIComponent(env.HD_PATH_CONSULTATION_ID));\nif (env.HD_QUERY_STRING) url += \"?\" + env.HD_QUERY_STRING;\nconst headers = { \"Accept\": \"application/json\" };\nheaders.Authorization = \"Bearer \" + env.HD_PARTNER_API_KEY;\nconst options = { method: \"GET\", headers, redirect: \"manual\", signal: AbortSignal.timeout(30000) };\nconst response = await fetch(url, options);\nwriteFileSync(env.HD_RESPONSE_FILE, Buffer.from(await response.arrayBuffer()), {mode: 0o600});\nwriteFileSync(env.HD_HEADERS_FILE, JSON.stringify(Object.fromEntries(response.headers)), {mode: 0o600});\nconsole.log(response.status);\nconsole.log(response.headers.get(\"content-type\") ?? \"\");\n"}]}},"/v1/partner/consultations/{consultation_id}/info-requests/{request_id}/answer/":{"post":{"operationId":"answer_consultation_info_request","description":"Answers an open information request with the requested question keys and, when documents were requested, upload ids (at most 10, each used once). A request with reason_code identity_document_required is answered with identity_document (it may carry no question keys); the document is validated, stored on the patient's account in live mode and never echoed. The consultation returns to review once answered; a retry after a successful answer is refused with 409 info_request_not_open.\n\nChecks run in this order: the request must exist (404), be open (409 info_request_not_open, also for a request withdrawn by a consent withdrawal), the patient must not have withdrawn consent (409 consent_withdrawn) and the consultation must still await information (409 case_closed). Then the body must carry at least one answer, one upload or the identity document: an empty body is refused with 422 invalid_request before identity_document_required or any other check of its contents. Requires the consultations:write scope.\n\n### Access, effects and retries\n**Ownership:** The API key's own partner and mode (test or live); another partner's or mode's records answer 404.\n\n**Prerequisites:** A partner API key with the `consultations:write` scope.\n\n**Effects:** Stores the answers, attaches the uploads and returns the consultation to review; emits a consultation webhook event.\n\n**Retries:** A retry after a successful answer answers 409 info_request_not_open.","summary":"Answer an information request","parameters":[{"in":"path","name":"consultation_id","schema":{"type":"string","format":"uuid"},"description":"The consultation id.","required":true,"example":"7f8b1c2e-4d5a-4e6f-9a0b-1c2d3e4f5a6b"},{"in":"path","name":"request_id","schema":{"type":"string","format":"uuid"},"description":"The information request id.","required":true,"example":"2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e"},{"in":"query","name":"locale","schema":{"type":"string"},"description":"Language for the question labels: one of en, es, zh. Defaults to the partner's default locale; any other value falls back to English.","example":"en"},{"in":"header","name":"Idempotency-Key","schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{8,128}$"},"description":"Accepted but not used for replay on this operation. Only POST /v1/partner/consultations stores and replays responses by Idempotency-Key.","example":"b7c1d2e3f4a5b6c7"}],"tags":["Information requests"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InfoRequestAnswerRequest"},"examples":{"example-1":{"value":{"answers":[{"question_key":"scalp_photo_top","value":"3d4e5f60-7182-4934-a5b6-c7d8e9f0a1b2"}],"uploads":[]},"summary":"Illustrative request example"},"example-2":{"value":{"identity_document":{"type":"id_card","number":"12345678Z","issuing_state":"ES"}},"summary":"Illustrative request example"}}}},"required":true},"security":[{"PartnerApiKey":[]}],"responses":{"200":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InfoRequest"},"examples":{"example-1":{"value":{"id":"9c8b7a69-5847-4362-9150-4f3e2d1c0b0a","object":"info_request","consultation_id":"7f8b1c2e-4d5a-4e6f-9a0b-1c2d3e4f5a6b","status":"answered","reason_code":"photo_quality","reason_label":"A photo is not clear enough for a clinical review.","question_keys":["scalp_photo_top"],"questions":[{"key":"scalp_photo_top","label":"Photo of the top of your head","type":"upload"}],"documents_requested":[],"requested_at":"2026-11-03T11:20:00Z","answered_at":"2026-11-03T12:40:00Z","withdrawn_at":null,"answered_question_keys":["scalp_photo_top"],"answer_count":1,"answer_document_count":1},"summary":"Illustrative response example"}}}},"description":"The answered information request."},"400":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"invalid_request","code":"parse_error","message":"JSON parse error - Expecting value: line 1 column 1 (char 0)","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"invalid_request","code":"brand_header_not_allowed","message":"Brand headers are not accepted on partner requests; the brand comes from the API key.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request is malformed. Partner routes also refuse the patient-app brand header with 400 brand_header_not_allowed, before authentication (no rate-limit headers)."},"401":{"headers":{"WWW-Authenticate":{"schema":{"type":"string"},"description":"The authentication scheme to use: Bearer realm=\"hi-doctor-partner\"."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"authentication_error","code":"not_authenticated","message":"Authentication credentials were not provided.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"authentication_error","code":"invalid_token","message":"The API key is not valid.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"authentication_error","code":"key_revoked","message":"The API key has been revoked.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"authentication_error","code":"key_expired","message":"The API key has expired.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"authentication_error","code":"partner_suspended","message":"The partner account is suspended.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The API key is missing, malformed, unknown, revoked or expired, or the partner account is suspended. The response carries WWW-Authenticate: Bearer realm=\"hi-doctor-partner\" and no rate-limit headers."},"403":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"permission_error","code":"insufficient_scope","message":"This endpoint requires the consultations:write scope.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"permission_error","code":"partner_not_live","message":"Live mode is not enabled for this partner.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The key lacks the consultations:write scope (insufficient_scope), or a live key was used while live mode is not enabled for the partner (partner_not_live)."},"404":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PartnerError"},{"$ref":"#/components/schemas/PartnerFlagOffError"}]},"examples":{"example-1":{"value":{"error":{"type":"not_found","code":"not_found","message":"No such resource.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"not_found"}},"summary":"Illustrative response example"}}}},"description":"No such resource for this key's partner and mode. While the Partner API is switched off for the environment, every route answers 404 with only {\"error\": {\"type\": \"not_found\"}}, before authentication."},"409":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"conflict","code":"info_request_not_open","message":"The information request is no longer open.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"conflict","code":"patient_conflict","message":"The identity document does not match the patient's account.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"conflict","code":"consent_withdrawn","message":"The patient withdrew consent; the consultation is not processed.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"conflict","code":"case_closed","message":"The consultation is no longer awaiting information.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"conflict","code":"consultation_busy","message":"The consultation is busy; retry shortly.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request conflicts with the information request's current state. A request withdrawn by a consent withdrawal is refused with info_request_not_open (the request's status is checked first); consent_withdrawn is returned only for a request still open for a patient who withdrew consent."},"422":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"validation_error","code":"unexpected_question_key","message":"Only the questions named in the information request may be answered.","errors":[{"question_key":"hair_loss_pattern","code":"unexpected_question_key","message":"Only the questions named in the information request may be answered."}],"request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"validation_error","code":"identity_document_required","message":"The answer to this information request needs the identity document.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"validation_error","code":"invalid_request","message":"The request body is invalid.","errors":[{"field":"identity_document","code":"invalid_identity_document","message":"Enter a valid document number for this document type and issuing state."}],"request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"validation_error","code":"invalid_request","message":"The request body is invalid.","errors":[{"field":"identity_document.type","code":"invalid_choice","message":"This is not a valid choice."}],"request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"validation_error","code":"invalid_request","message":"The request body is invalid.","errors":[{"field":"identity_document","code":"unexpected_field","message":"identity_document is accepted only on an identity_document_required request."}],"request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-6":{"value":{"error":{"type":"validation_error","code":"invalid_request","message":"Send at least one answer, one upload or the identity document.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-7":{"value":{"error":{"type":"validation_error","code":"invalid_request","message":"answers must be a list of entries-v1 objects.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-8":{"value":{"error":{"type":"validation_error","code":"invalid_request","message":"uploads must be a list of upload ids.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-9":{"value":{"error":{"type":"validation_error","code":"invalid_request","message":"At most 10 uploads per answer.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-10":{"value":{"error":{"type":"validation_error","code":"invalid_answers","message":"Each question may be answered once.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-11":{"value":{"error":{"type":"validation_error","code":"out_of_range","message":"A value is outside the schema's input bounds.","errors":[{"question_key":"body_measurements","field":"height","code":"out_of_range","message":"The value is outside the allowed range.","min":100,"max":250}],"request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-12":{"value":{"error":{"type":"validation_error","code":"upload_unusable","message":"Each upload can be used once.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request body is invalid."},"429":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"rate_limited","code":"throttled","message":"Request was throttled. Expected available in 12 seconds.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"Too many requests; wait for the number of seconds in Retry-After."}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"set -eu\numask 077\n# Path values must be URL-encoded; query string excludes the leading question mark.\nurl=\"${HD_BASE_URL:?Set API origin}/v1/partner/consultations/${HD_PATH_CONSULTATION_ID:?Set the path fixture}/info-requests/${HD_PATH_REQUEST_ID:?Set the path fixture}/answer/${HD_QUERY_STRING:+?${HD_QUERY_STRING}}\"\nprintf 'Accept: application/json\\n' > \"${HD_REQUEST_HEADERS_FILE:?Private header file}\"\nprintf 'Authorization: Bearer %s\\n' \"${HD_PARTNER_API_KEY:?Set partner API key}\" >> \"$HD_REQUEST_HEADERS_FILE\"\ncurl --silent --show-error --max-time 30 --request POST \\\n  --header \"@$HD_REQUEST_HEADERS_FILE\" \\\n  --dump-header \"${HD_HEADERS_FILE:?Private response header file}\" \\\n  --output \"${HD_RESPONSE_FILE:?Private response file}\" \\\n  --write-out '%{http_code}\\n%{content_type}\\n' \\\n  --header 'Content-Type: application/json' \\\n  --data-binary \"@${HD_REQUEST_FILE:?JSON fixture file}\" \\\n  \"$url\"\n"},{"lang":"Python","label":"requests","source":"import os, json\nfrom pathlib import Path\nfrom urllib.parse import quote\nimport requests\nos.umask(0o077)\nurl = os.environ[\"HD_BASE_URL\"].rstrip(\"/\") + '/v1/partner/consultations/{consultation_id}/info-requests/{request_id}/answer/'\nurl = url.replace('{consultation_id}', quote(os.environ['HD_PATH_CONSULTATION_ID'], safe=\"\"))\nurl = url.replace('{request_id}', quote(os.environ['HD_PATH_REQUEST_ID'], safe=\"\"))\nif os.environ.get(\"HD_QUERY_STRING\"): url += \"?\" + os.environ[\"HD_QUERY_STRING\"]\nheaders = {\"Accept\": 'application/json'}\nheaders[\"Authorization\"] = \"Bearer \" + os.environ[\"HD_PARTNER_API_KEY\"]\nkwargs = {}\nkwargs[\"json\"] = json.loads(Path(os.environ[\"HD_REQUEST_FILE\"]).read_text())\nresponse = requests.request('POST', url, headers=headers, timeout=30, allow_redirects=False, **kwargs)\nPath(os.environ[\"HD_RESPONSE_FILE\"]).write_bytes(response.content)\nPath(os.environ[\"HD_HEADERS_FILE\"]).write_text(json.dumps(dict(response.headers)))\nprint(response.status_code)\nprint(response.headers.get(\"Content-Type\", \"\"))\n"},{"lang":"JavaScript","label":"fetch","source":"import { readFileSync, writeFileSync } from \"node:fs\";\nconst env = process.env;\nlet url = env.HD_BASE_URL.replace(/\\/$/, \"\") + \"/v1/partner/consultations/{consultation_id}/info-requests/{request_id}/answer/\";\nurl = url.replace(\"{consultation_id}\", encodeURIComponent(env.HD_PATH_CONSULTATION_ID));\nurl = url.replace(\"{request_id}\", encodeURIComponent(env.HD_PATH_REQUEST_ID));\nif (env.HD_QUERY_STRING) url += \"?\" + env.HD_QUERY_STRING;\nconst headers = { \"Accept\": \"application/json\" };\nheaders.Authorization = \"Bearer \" + env.HD_PARTNER_API_KEY;\nconst options = { method: \"POST\", headers, redirect: \"manual\", signal: AbortSignal.timeout(30000) };\nheaders[\"Content-Type\"] = \"application/json\";\noptions.body = readFileSync(env.HD_REQUEST_FILE, \"utf8\");\nconst response = await fetch(url, options);\nwriteFileSync(env.HD_RESPONSE_FILE, Buffer.from(await response.arrayBuffer()), {mode: 0o600});\nwriteFileSync(env.HD_HEADERS_FILE, JSON.stringify(Object.fromEntries(response.headers)), {mode: 0o600});\nconsole.log(response.status);\nconsole.log(response.headers.get(\"content-type\") ?? \"\");\n"}]}},"/v1/partner/info-requests/{request_id}/answer/":{"post":{"operationId":"answer_info_request","description":"Answers an open information request with the requested question keys and, when documents were requested, upload ids (at most 10, each used once). A request with reason_code identity_document_required is answered with identity_document (it may carry no question keys); the document is validated, stored on the patient's account in live mode and never echoed. The consultation returns to review once answered; a retry after a successful answer is refused with 409 info_request_not_open.\n\nChecks run in this order: the request must exist (404), be open (409 info_request_not_open, also for a request withdrawn by a consent withdrawal), the patient must not have withdrawn consent (409 consent_withdrawn) and the consultation must still await information (409 case_closed). Then the body must carry at least one answer, one upload or the identity document: an empty body is refused with 422 invalid_request before identity_document_required or any other check of its contents. Requires the consultations:write scope. Same as the nested route, addressed by the request id alone.\n\n### Access, effects and retries\n**Ownership:** The API key's own partner and mode (test or live); another partner's or mode's records answer 404.\n\n**Prerequisites:** A partner API key with the `consultations:write` scope.\n\n**Effects:** Stores the answers, attaches the uploads and returns the consultation to review; emits a consultation webhook event.\n\n**Retries:** A retry after a successful answer answers 409 info_request_not_open.","summary":"Answer an information request by id","parameters":[{"in":"path","name":"request_id","schema":{"type":"string","format":"uuid"},"description":"The information request id.","required":true,"example":"2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e"},{"in":"query","name":"locale","schema":{"type":"string"},"description":"Language for the question labels: one of en, es, zh. Defaults to the partner's default locale; any other value falls back to English.","example":"en"},{"in":"header","name":"Idempotency-Key","schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{8,128}$"},"description":"Accepted but not used for replay on this operation. Only POST /v1/partner/consultations stores and replays responses by Idempotency-Key.","example":"b7c1d2e3f4a5b6c7"}],"tags":["Information requests"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InfoRequestAnswerRequest"},"examples":{"example-1":{"value":{"answers":[{"question_key":"scalp_photo_top","value":"3d4e5f60-7182-4934-a5b6-c7d8e9f0a1b2"}],"uploads":[]},"summary":"Illustrative request example"},"example-2":{"value":{"identity_document":{"type":"id_card","number":"12345678Z","issuing_state":"ES"}},"summary":"Illustrative request example"}}}},"required":true},"security":[{"PartnerApiKey":[]}],"responses":{"200":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InfoRequest"},"examples":{"example-1":{"value":{"id":"9c8b7a69-5847-4362-9150-4f3e2d1c0b0a","object":"info_request","consultation_id":"7f8b1c2e-4d5a-4e6f-9a0b-1c2d3e4f5a6b","status":"answered","reason_code":"photo_quality","reason_label":"A photo is not clear enough for a clinical review.","question_keys":["scalp_photo_top"],"questions":[{"key":"scalp_photo_top","label":"Photo of the top of your head","type":"upload"}],"documents_requested":[],"requested_at":"2026-11-03T11:20:00Z","answered_at":"2026-11-03T12:40:00Z","withdrawn_at":null,"answered_question_keys":["scalp_photo_top"],"answer_count":1,"answer_document_count":1},"summary":"Illustrative response example"}}}},"description":"The answered information request."},"400":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"invalid_request","code":"parse_error","message":"JSON parse error - Expecting value: line 1 column 1 (char 0)","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"invalid_request","code":"brand_header_not_allowed","message":"Brand headers are not accepted on partner requests; the brand comes from the API key.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request is malformed. Partner routes also refuse the patient-app brand header with 400 brand_header_not_allowed, before authentication (no rate-limit headers)."},"401":{"headers":{"WWW-Authenticate":{"schema":{"type":"string"},"description":"The authentication scheme to use: Bearer realm=\"hi-doctor-partner\"."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"authentication_error","code":"not_authenticated","message":"Authentication credentials were not provided.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"authentication_error","code":"invalid_token","message":"The API key is not valid.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"authentication_error","code":"key_revoked","message":"The API key has been revoked.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"authentication_error","code":"key_expired","message":"The API key has expired.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"authentication_error","code":"partner_suspended","message":"The partner account is suspended.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The API key is missing, malformed, unknown, revoked or expired, or the partner account is suspended. The response carries WWW-Authenticate: Bearer realm=\"hi-doctor-partner\" and no rate-limit headers."},"403":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"permission_error","code":"insufficient_scope","message":"This endpoint requires the consultations:write scope.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"permission_error","code":"partner_not_live","message":"Live mode is not enabled for this partner.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The key lacks the consultations:write scope (insufficient_scope), or a live key was used while live mode is not enabled for the partner (partner_not_live)."},"404":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PartnerError"},{"$ref":"#/components/schemas/PartnerFlagOffError"}]},"examples":{"example-1":{"value":{"error":{"type":"not_found","code":"not_found","message":"No such resource.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"not_found"}},"summary":"Illustrative response example"}}}},"description":"No such resource for this key's partner and mode. While the Partner API is switched off for the environment, every route answers 404 with only {\"error\": {\"type\": \"not_found\"}}, before authentication."},"409":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"conflict","code":"info_request_not_open","message":"The information request is no longer open.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"conflict","code":"patient_conflict","message":"The identity document does not match the patient's account.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"conflict","code":"consent_withdrawn","message":"The patient withdrew consent; the consultation is not processed.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"conflict","code":"case_closed","message":"The consultation is no longer awaiting information.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"conflict","code":"consultation_busy","message":"The consultation is busy; retry shortly.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request conflicts with the information request's current state. A request withdrawn by a consent withdrawal is refused with info_request_not_open (the request's status is checked first); consent_withdrawn is returned only for a request still open for a patient who withdrew consent."},"422":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"validation_error","code":"unexpected_question_key","message":"Only the questions named in the information request may be answered.","errors":[{"question_key":"hair_loss_pattern","code":"unexpected_question_key","message":"Only the questions named in the information request may be answered."}],"request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"validation_error","code":"identity_document_required","message":"The answer to this information request needs the identity document.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"validation_error","code":"invalid_request","message":"The request body is invalid.","errors":[{"field":"identity_document","code":"invalid_identity_document","message":"Enter a valid document number for this document type and issuing state."}],"request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"validation_error","code":"invalid_request","message":"The request body is invalid.","errors":[{"field":"identity_document.type","code":"invalid_choice","message":"This is not a valid choice."}],"request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"validation_error","code":"invalid_request","message":"The request body is invalid.","errors":[{"field":"identity_document","code":"unexpected_field","message":"identity_document is accepted only on an identity_document_required request."}],"request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-6":{"value":{"error":{"type":"validation_error","code":"invalid_request","message":"Send at least one answer, one upload or the identity document.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-7":{"value":{"error":{"type":"validation_error","code":"invalid_request","message":"answers must be a list of entries-v1 objects.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-8":{"value":{"error":{"type":"validation_error","code":"invalid_request","message":"uploads must be a list of upload ids.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-9":{"value":{"error":{"type":"validation_error","code":"invalid_request","message":"At most 10 uploads per answer.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-10":{"value":{"error":{"type":"validation_error","code":"invalid_answers","message":"Each question may be answered once.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-11":{"value":{"error":{"type":"validation_error","code":"out_of_range","message":"A value is outside the schema's input bounds.","errors":[{"question_key":"body_measurements","field":"height","code":"out_of_range","message":"The value is outside the allowed range.","min":100,"max":250}],"request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-12":{"value":{"error":{"type":"validation_error","code":"upload_unusable","message":"Each upload can be used once.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request body is invalid."},"429":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"rate_limited","code":"throttled","message":"Request was throttled. Expected available in 12 seconds.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"Too many requests; wait for the number of seconds in Retry-After."}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"set -eu\numask 077\n# Path values must be URL-encoded; query string excludes the leading question mark.\nurl=\"${HD_BASE_URL:?Set API origin}/v1/partner/info-requests/${HD_PATH_REQUEST_ID:?Set the path fixture}/answer/${HD_QUERY_STRING:+?${HD_QUERY_STRING}}\"\nprintf 'Accept: application/json\\n' > \"${HD_REQUEST_HEADERS_FILE:?Private header file}\"\nprintf 'Authorization: Bearer %s\\n' \"${HD_PARTNER_API_KEY:?Set partner API key}\" >> \"$HD_REQUEST_HEADERS_FILE\"\ncurl --silent --show-error --max-time 30 --request POST \\\n  --header \"@$HD_REQUEST_HEADERS_FILE\" \\\n  --dump-header \"${HD_HEADERS_FILE:?Private response header file}\" \\\n  --output \"${HD_RESPONSE_FILE:?Private response file}\" \\\n  --write-out '%{http_code}\\n%{content_type}\\n' \\\n  --header 'Content-Type: application/json' \\\n  --data-binary \"@${HD_REQUEST_FILE:?JSON fixture file}\" \\\n  \"$url\"\n"},{"lang":"Python","label":"requests","source":"import os, json\nfrom pathlib import Path\nfrom urllib.parse import quote\nimport requests\nos.umask(0o077)\nurl = os.environ[\"HD_BASE_URL\"].rstrip(\"/\") + '/v1/partner/info-requests/{request_id}/answer/'\nurl = url.replace('{request_id}', quote(os.environ['HD_PATH_REQUEST_ID'], safe=\"\"))\nif os.environ.get(\"HD_QUERY_STRING\"): url += \"?\" + os.environ[\"HD_QUERY_STRING\"]\nheaders = {\"Accept\": 'application/json'}\nheaders[\"Authorization\"] = \"Bearer \" + os.environ[\"HD_PARTNER_API_KEY\"]\nkwargs = {}\nkwargs[\"json\"] = json.loads(Path(os.environ[\"HD_REQUEST_FILE\"]).read_text())\nresponse = requests.request('POST', url, headers=headers, timeout=30, allow_redirects=False, **kwargs)\nPath(os.environ[\"HD_RESPONSE_FILE\"]).write_bytes(response.content)\nPath(os.environ[\"HD_HEADERS_FILE\"]).write_text(json.dumps(dict(response.headers)))\nprint(response.status_code)\nprint(response.headers.get(\"Content-Type\", \"\"))\n"},{"lang":"JavaScript","label":"fetch","source":"import { readFileSync, writeFileSync } from \"node:fs\";\nconst env = process.env;\nlet url = env.HD_BASE_URL.replace(/\\/$/, \"\") + \"/v1/partner/info-requests/{request_id}/answer/\";\nurl = url.replace(\"{request_id}\", encodeURIComponent(env.HD_PATH_REQUEST_ID));\nif (env.HD_QUERY_STRING) url += \"?\" + env.HD_QUERY_STRING;\nconst headers = { \"Accept\": \"application/json\" };\nheaders.Authorization = \"Bearer \" + env.HD_PARTNER_API_KEY;\nconst options = { method: \"POST\", headers, redirect: \"manual\", signal: AbortSignal.timeout(30000) };\nheaders[\"Content-Type\"] = \"application/json\";\noptions.body = readFileSync(env.HD_REQUEST_FILE, \"utf8\");\nconst response = await fetch(url, options);\nwriteFileSync(env.HD_RESPONSE_FILE, Buffer.from(await response.arrayBuffer()), {mode: 0o600});\nwriteFileSync(env.HD_HEADERS_FILE, JSON.stringify(Object.fromEntries(response.headers)), {mode: 0o600});\nconsole.log(response.status);\nconsole.log(response.headers.get(\"content-type\") ?? \"\");\n"}]}},"/v1/partner/prescriptions/":{"get":{"operationId":"list_prescriptions","description":"Prescriptions issued for your consultations in this key's mode, newest first. The patient receives the prescription itself through the route in fulfilment.route. Requires the prescriptions:read scope.\n\n### Access, effects and retries\n**Ownership:** The API key's own partner and mode (test or live).\n\n**Prerequisites:** A partner API key with the `prescriptions:read` scope.\n\n**Effects:** None.\n\n**Retries:** Safe to retry.","summary":"List prescriptions","parameters":[{"in":"query","name":"consultation_id","schema":{"type":"string","format":"uuid"},"description":"Only prescriptions for this consultation.","example":"7f8b1c2e-4d5a-4e6f-9a0b-1c2d3e4f5a6b"},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Page size, 1 to 100. Default 50. A value outside the range is refused with 400 invalid_request.","example":50},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"The next_cursor of the previous page.","example":"MjAyNi0xMS0wM1QxMDoxNTowMCswMDowMHw3ZjhiMWMyZS00ZDVhLTRlNmYtOWEwYi0xYzJkM2U0ZjVhNmI="}],"tags":["Prescriptions"],"security":[{"PartnerApiKey":[]}],"responses":{"200":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PrescriptionList"},"examples":{"example-1":{"value":{"object":"list","results":[{"id":"5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d","consultation_id":"7f8b1c2e-4d5a-4e6f-9a0b-1c2d3e4f5a6b","status":"issued","active":true,"issued_at":"2026-11-03T12:40:00Z","cancelled_at":null,"expires_at":"2027-05-03T12:40:00Z","dispensing_country":"ES","medication":{"name":"Finasteride","prescribed_label":"Finasteride 1 mg","dosage":"1 mg","supply_days":90},"fulfilment":{"route":"national_e_prescription"},"collected_at":null,"replaced_by":null}],"next_cursor":null},"summary":"Illustrative response example"}}}},"description":"Success."},"400":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"invalid_request","code":"invalid_request","message":"limit must be between 1 and 100.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"invalid_request","code":"invalid_cursor","message":"The pagination cursor is not valid.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"invalid_request","code":"invalid_request","message":"consultation_id must be a UUID.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"invalid_request","code":"brand_header_not_allowed","message":"Brand headers are not accepted on partner requests; the brand comes from the API key.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request is malformed. Partner routes also refuse the patient-app brand header with 400 brand_header_not_allowed, before authentication (no rate-limit headers)."},"401":{"headers":{"WWW-Authenticate":{"schema":{"type":"string"},"description":"The authentication scheme to use: Bearer realm=\"hi-doctor-partner\"."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"authentication_error","code":"not_authenticated","message":"Authentication credentials were not provided.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"authentication_error","code":"invalid_token","message":"The API key is not valid.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"authentication_error","code":"key_revoked","message":"The API key has been revoked.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"authentication_error","code":"key_expired","message":"The API key has expired.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"authentication_error","code":"partner_suspended","message":"The partner account is suspended.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The API key is missing, malformed, unknown, revoked or expired, or the partner account is suspended. The response carries WWW-Authenticate: Bearer realm=\"hi-doctor-partner\" and no rate-limit headers."},"403":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"permission_error","code":"insufficient_scope","message":"This endpoint requires the prescriptions:read scope.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"permission_error","code":"partner_not_live","message":"Live mode is not enabled for this partner.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The key lacks the prescriptions:read scope (insufficient_scope), or a live key was used while live mode is not enabled for the partner (partner_not_live)."},"404":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PartnerError"},{"$ref":"#/components/schemas/PartnerFlagOffError"}]},"examples":{"example-1":{"value":{"error":{"type":"not_found"}},"summary":"Illustrative response example"}}}},"description":"While the Partner API is switched off for the environment, every route answers 404 with only {\"error\": {\"type\": \"not_found\"}}, before authentication."},"429":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"rate_limited","code":"throttled","message":"Request was throttled. Expected available in 12 seconds.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"Too many requests; wait for the number of seconds in Retry-After."}},"x-pagination":{"kind":"cursor","defaultLimit":50,"maxLimit":100,"ordering":["-created_at"],"defaultOrdering":"-created_at"},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"set -eu\numask 077\n# Path values must be URL-encoded; query string excludes the leading question mark.\nurl=\"${HD_BASE_URL:?Set API origin}/v1/partner/prescriptions/${HD_QUERY_STRING:+?${HD_QUERY_STRING}}\"\nprintf 'Accept: application/json\\n' > \"${HD_REQUEST_HEADERS_FILE:?Private header file}\"\nprintf 'Authorization: Bearer %s\\n' \"${HD_PARTNER_API_KEY:?Set partner API key}\" >> \"$HD_REQUEST_HEADERS_FILE\"\ncurl --silent --show-error --max-time 30 --request GET \\\n  --header \"@$HD_REQUEST_HEADERS_FILE\" \\\n  --dump-header \"${HD_HEADERS_FILE:?Private response header file}\" \\\n  --output \"${HD_RESPONSE_FILE:?Private response file}\" \\\n  --write-out '%{http_code}\\n%{content_type}\\n' \\\n  \"$url\"\n"},{"lang":"Python","label":"requests","source":"import os, json\nfrom pathlib import Path\nfrom urllib.parse import quote\nimport requests\nos.umask(0o077)\nurl = os.environ[\"HD_BASE_URL\"].rstrip(\"/\") + '/v1/partner/prescriptions/'\nif os.environ.get(\"HD_QUERY_STRING\"): url += \"?\" + os.environ[\"HD_QUERY_STRING\"]\nheaders = {\"Accept\": 'application/json'}\nheaders[\"Authorization\"] = \"Bearer \" + os.environ[\"HD_PARTNER_API_KEY\"]\nkwargs = {}\nresponse = requests.request('GET', url, headers=headers, timeout=30, allow_redirects=False, **kwargs)\nPath(os.environ[\"HD_RESPONSE_FILE\"]).write_bytes(response.content)\nPath(os.environ[\"HD_HEADERS_FILE\"]).write_text(json.dumps(dict(response.headers)))\nprint(response.status_code)\nprint(response.headers.get(\"Content-Type\", \"\"))\n"},{"lang":"JavaScript","label":"fetch","source":"import { readFileSync, writeFileSync } from \"node:fs\";\nconst env = process.env;\nlet url = env.HD_BASE_URL.replace(/\\/$/, \"\") + \"/v1/partner/prescriptions/\";\nif (env.HD_QUERY_STRING) url += \"?\" + env.HD_QUERY_STRING;\nconst headers = { \"Accept\": \"application/json\" };\nheaders.Authorization = \"Bearer \" + env.HD_PARTNER_API_KEY;\nconst options = { method: \"GET\", headers, redirect: \"manual\", signal: AbortSignal.timeout(30000) };\nconst response = await fetch(url, options);\nwriteFileSync(env.HD_RESPONSE_FILE, Buffer.from(await response.arrayBuffer()), {mode: 0o600});\nwriteFileSync(env.HD_HEADERS_FILE, JSON.stringify(Object.fromEntries(response.headers)), {mode: 0o600});\nconsole.log(response.status);\nconsole.log(response.headers.get(\"content-type\") ?? \"\");\n"}]}},"/v1/partner/prescriptions/{prescription_id}/":{"get":{"operationId":"get_prescription","description":"One prescription with its medication, validity and fulfilment route. Requires the prescriptions:read scope.\n\n### Access, effects and retries\n**Ownership:** The API key's own partner and mode (test or live); another partner's or mode's records answer 404.\n\n**Prerequisites:** A partner API key with the `prescriptions:read` scope.\n\n**Effects:** None.\n\n**Retries:** Safe to retry.","summary":"Get a prescription","parameters":[{"in":"path","name":"prescription_id","schema":{"type":"string","format":"uuid"},"description":"The prescription id.","required":true,"example":"5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d"}],"tags":["Prescriptions"],"security":[{"PartnerApiKey":[]}],"responses":{"200":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Prescription"},"examples":{"example-1":{"value":{"id":"5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d","consultation_id":"7f8b1c2e-4d5a-4e6f-9a0b-1c2d3e4f5a6b","status":"issued","active":true,"issued_at":"2026-11-03T12:40:00Z","cancelled_at":null,"expires_at":"2027-05-03T12:40:00Z","dispensing_country":"ES","medication":{"name":"Finasteride","prescribed_label":"Finasteride 1 mg","dosage":"1 mg","supply_days":90},"fulfilment":{"route":"national_e_prescription"},"collected_at":null,"replaced_by":null},"summary":"Illustrative response example"}}}},"description":"Success."},"400":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"invalid_request","code":"brand_header_not_allowed","message":"Brand headers are not accepted on partner requests; the brand comes from the API key.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request is malformed. Partner routes also refuse the patient-app brand header with 400 brand_header_not_allowed, before authentication (no rate-limit headers)."},"401":{"headers":{"WWW-Authenticate":{"schema":{"type":"string"},"description":"The authentication scheme to use: Bearer realm=\"hi-doctor-partner\"."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"authentication_error","code":"not_authenticated","message":"Authentication credentials were not provided.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"authentication_error","code":"invalid_token","message":"The API key is not valid.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"authentication_error","code":"key_revoked","message":"The API key has been revoked.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"authentication_error","code":"key_expired","message":"The API key has expired.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"authentication_error","code":"partner_suspended","message":"The partner account is suspended.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The API key is missing, malformed, unknown, revoked or expired, or the partner account is suspended. The response carries WWW-Authenticate: Bearer realm=\"hi-doctor-partner\" and no rate-limit headers."},"403":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"permission_error","code":"insufficient_scope","message":"This endpoint requires the prescriptions:read scope.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"permission_error","code":"partner_not_live","message":"Live mode is not enabled for this partner.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The key lacks the prescriptions:read scope (insufficient_scope), or a live key was used while live mode is not enabled for the partner (partner_not_live)."},"404":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PartnerError"},{"$ref":"#/components/schemas/PartnerFlagOffError"}]},"examples":{"example-1":{"value":{"error":{"type":"not_found","code":"not_found","message":"No such resource.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"not_found"}},"summary":"Illustrative response example"}}}},"description":"No such resource for this key's partner and mode. While the Partner API is switched off for the environment, every route answers 404 with only {\"error\": {\"type\": \"not_found\"}}, before authentication."},"429":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"rate_limited","code":"throttled","message":"Request was throttled. Expected available in 12 seconds.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"Too many requests; wait for the number of seconds in Retry-After."}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"set -eu\numask 077\n# Path values must be URL-encoded; query string excludes the leading question mark.\nurl=\"${HD_BASE_URL:?Set API origin}/v1/partner/prescriptions/${HD_PATH_PRESCRIPTION_ID:?Set the path fixture}/${HD_QUERY_STRING:+?${HD_QUERY_STRING}}\"\nprintf 'Accept: application/json\\n' > \"${HD_REQUEST_HEADERS_FILE:?Private header file}\"\nprintf 'Authorization: Bearer %s\\n' \"${HD_PARTNER_API_KEY:?Set partner API key}\" >> \"$HD_REQUEST_HEADERS_FILE\"\ncurl --silent --show-error --max-time 30 --request GET \\\n  --header \"@$HD_REQUEST_HEADERS_FILE\" \\\n  --dump-header \"${HD_HEADERS_FILE:?Private response header file}\" \\\n  --output \"${HD_RESPONSE_FILE:?Private response file}\" \\\n  --write-out '%{http_code}\\n%{content_type}\\n' \\\n  \"$url\"\n"},{"lang":"Python","label":"requests","source":"import os, json\nfrom pathlib import Path\nfrom urllib.parse import quote\nimport requests\nos.umask(0o077)\nurl = os.environ[\"HD_BASE_URL\"].rstrip(\"/\") + '/v1/partner/prescriptions/{prescription_id}/'\nurl = url.replace('{prescription_id}', quote(os.environ['HD_PATH_PRESCRIPTION_ID'], safe=\"\"))\nif os.environ.get(\"HD_QUERY_STRING\"): url += \"?\" + os.environ[\"HD_QUERY_STRING\"]\nheaders = {\"Accept\": 'application/json'}\nheaders[\"Authorization\"] = \"Bearer \" + os.environ[\"HD_PARTNER_API_KEY\"]\nkwargs = {}\nresponse = requests.request('GET', url, headers=headers, timeout=30, allow_redirects=False, **kwargs)\nPath(os.environ[\"HD_RESPONSE_FILE\"]).write_bytes(response.content)\nPath(os.environ[\"HD_HEADERS_FILE\"]).write_text(json.dumps(dict(response.headers)))\nprint(response.status_code)\nprint(response.headers.get(\"Content-Type\", \"\"))\n"},{"lang":"JavaScript","label":"fetch","source":"import { readFileSync, writeFileSync } from \"node:fs\";\nconst env = process.env;\nlet url = env.HD_BASE_URL.replace(/\\/$/, \"\") + \"/v1/partner/prescriptions/{prescription_id}/\";\nurl = url.replace(\"{prescription_id}\", encodeURIComponent(env.HD_PATH_PRESCRIPTION_ID));\nif (env.HD_QUERY_STRING) url += \"?\" + env.HD_QUERY_STRING;\nconst headers = { \"Accept\": \"application/json\" };\nheaders.Authorization = \"Bearer \" + env.HD_PARTNER_API_KEY;\nconst options = { method: \"GET\", headers, redirect: \"manual\", signal: AbortSignal.timeout(30000) };\nconst response = await fetch(url, options);\nwriteFileSync(env.HD_RESPONSE_FILE, Buffer.from(await response.arrayBuffer()), {mode: 0o600});\nwriteFileSync(env.HD_HEADERS_FILE, JSON.stringify(Object.fromEntries(response.headers)), {mode: 0o600});\nconsole.log(response.status);\nconsole.log(response.headers.get(\"content-type\") ?? \"\");\n"}]}},"/v1/partner/webhook-endpoints/":{"get":{"operationId":"list_webhook_endpoints","description":"Webhook endpoints in this key's mode. Requires the consultations:read, prescriptions:read and webhooks:manage scopes.\n\n### Access, effects and retries\n**Ownership:** The API key's own partner and mode (test or live).\n\n**Prerequisites:** A partner API key with the `webhooks:manage` and `consultations:read` and `prescriptions:read` scopes.\n\n**Effects:** None.\n\n**Retries:** Safe to retry.","summary":"List webhook endpoints","tags":["Webhooks"],"security":[{"PartnerApiKey":[]}],"responses":{"200":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointList"},"examples":{"example-1":{"value":{"object":"list","results":[{"id":"1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","object":"webhook_endpoint","url":"https://partner.example.com/hooks/hi-doctor","description":"Order system","mode":"test","events":["consultation.approved","consultation.rejected","prescription.issued"],"status":"enabled","failure_streak":0,"last_success_at":"2026-11-03T12:40:00Z","last_failure_at":null,"previous_secret_expires_at":null,"created_at":"2026-11-03T10:15:00Z","updated_at":"2026-11-03T10:15:00Z"}]},"summary":"Illustrative response example"}}}},"description":"Success."},"400":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"invalid_request","code":"brand_header_not_allowed","message":"Brand headers are not accepted on partner requests; the brand comes from the API key.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request is malformed. Partner routes also refuse the patient-app brand header with 400 brand_header_not_allowed, before authentication (no rate-limit headers)."},"401":{"headers":{"WWW-Authenticate":{"schema":{"type":"string"},"description":"The authentication scheme to use: Bearer realm=\"hi-doctor-partner\"."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"authentication_error","code":"not_authenticated","message":"Authentication credentials were not provided.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"authentication_error","code":"invalid_token","message":"The API key is not valid.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"authentication_error","code":"key_revoked","message":"The API key has been revoked.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"authentication_error","code":"key_expired","message":"The API key has expired.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"authentication_error","code":"partner_suspended","message":"The partner account is suspended.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The API key is missing, malformed, unknown, revoked or expired, or the partner account is suspended. The response carries WWW-Authenticate: Bearer realm=\"hi-doctor-partner\" and no rate-limit headers."},"403":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"permission_error","code":"insufficient_scope","message":"This endpoint requires the consultations:read, prescriptions:read, webhooks:manage scope.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"permission_error","code":"partner_not_live","message":"Live mode is not enabled for this partner.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The key lacks the consultations:read, prescriptions:read, webhooks:manage scopes (insufficient_scope), or a live key was used while live mode is not enabled for the partner (partner_not_live)."},"404":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PartnerError"},{"$ref":"#/components/schemas/PartnerFlagOffError"}]},"examples":{"example-1":{"value":{"error":{"type":"not_found"}},"summary":"Illustrative response example"}}}},"description":"While the Partner API is switched off for the environment, every route answers 404 with only {\"error\": {\"type\": \"not_found\"}}, before authentication."},"429":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"rate_limited","code":"throttled","message":"Request was throttled. Expected available in 12 seconds.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"Too many requests; wait for the number of seconds in Retry-After."}},"x-pagination":{"kind":"unpaginated_list_envelope"},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"set -eu\numask 077\n# Path values must be URL-encoded; query string excludes the leading question mark.\nurl=\"${HD_BASE_URL:?Set API origin}/v1/partner/webhook-endpoints/${HD_QUERY_STRING:+?${HD_QUERY_STRING}}\"\nprintf 'Accept: application/json\\n' > \"${HD_REQUEST_HEADERS_FILE:?Private header file}\"\nprintf 'Authorization: Bearer %s\\n' \"${HD_PARTNER_API_KEY:?Set partner API key}\" >> \"$HD_REQUEST_HEADERS_FILE\"\ncurl --silent --show-error --max-time 30 --request GET \\\n  --header \"@$HD_REQUEST_HEADERS_FILE\" \\\n  --dump-header \"${HD_HEADERS_FILE:?Private response header file}\" \\\n  --output \"${HD_RESPONSE_FILE:?Private response file}\" \\\n  --write-out '%{http_code}\\n%{content_type}\\n' \\\n  \"$url\"\n"},{"lang":"Python","label":"requests","source":"import os, json\nfrom pathlib import Path\nfrom urllib.parse import quote\nimport requests\nos.umask(0o077)\nurl = os.environ[\"HD_BASE_URL\"].rstrip(\"/\") + '/v1/partner/webhook-endpoints/'\nif os.environ.get(\"HD_QUERY_STRING\"): url += \"?\" + os.environ[\"HD_QUERY_STRING\"]\nheaders = {\"Accept\": 'application/json'}\nheaders[\"Authorization\"] = \"Bearer \" + os.environ[\"HD_PARTNER_API_KEY\"]\nkwargs = {}\nresponse = requests.request('GET', url, headers=headers, timeout=30, allow_redirects=False, **kwargs)\nPath(os.environ[\"HD_RESPONSE_FILE\"]).write_bytes(response.content)\nPath(os.environ[\"HD_HEADERS_FILE\"]).write_text(json.dumps(dict(response.headers)))\nprint(response.status_code)\nprint(response.headers.get(\"Content-Type\", \"\"))\n"},{"lang":"JavaScript","label":"fetch","source":"import { readFileSync, writeFileSync } from \"node:fs\";\nconst env = process.env;\nlet url = env.HD_BASE_URL.replace(/\\/$/, \"\") + \"/v1/partner/webhook-endpoints/\";\nif (env.HD_QUERY_STRING) url += \"?\" + env.HD_QUERY_STRING;\nconst headers = { \"Accept\": \"application/json\" };\nheaders.Authorization = \"Bearer \" + env.HD_PARTNER_API_KEY;\nconst options = { method: \"GET\", headers, redirect: \"manual\", signal: AbortSignal.timeout(30000) };\nconst response = await fetch(url, options);\nwriteFileSync(env.HD_RESPONSE_FILE, Buffer.from(await response.arrayBuffer()), {mode: 0o600});\nwriteFileSync(env.HD_HEADERS_FILE, JSON.stringify(Object.fromEntries(response.headers)), {mode: 0o600});\nconsole.log(response.status);\nconsole.log(response.headers.get(\"content-type\") ?? \"\");\n"}]},"post":{"operationId":"create_webhook_endpoint","description":"Registers an https URL on port 443 to receive the listed event types, or [\"*\"] for all of them: consultation.approved, consultation.cancelled, consultation.in_review, consultation.info_requested, consultation.rejected, consultation.submitted, consultation.updated, prescription.cancelled, prescription.issued, prescription.replaced. endpoint.test is sent only by POST /webhook-endpoints/{endpoint_id}/test and cannot be subscribed to (422 invalid_webhook_events). mode, when sent, must be the key's own mode (422 mode_mismatch). The response carries the signing secret once. Requires the consultations:read, prescriptions:read and webhooks:manage scopes.\n\n### Access, effects and retries\n**Ownership:** The API key's own partner and mode (test or live).\n\n**Prerequisites:** A partner API key with the `webhooks:manage` and `consultations:read` and `prescriptions:read` scopes.\n\n**Effects:** Registers the endpoint and creates its signing secret, returned once.\n\n**Retries:** Not replayed by Idempotency-Key: a retry registers a second endpoint.","summary":"Create a webhook endpoint","parameters":[{"in":"header","name":"Idempotency-Key","schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{8,128}$"},"description":"Accepted but not used for replay on this operation. Only POST /v1/partner/consultations stores and replays responses by Idempotency-Key.","example":"b7c1d2e3f4a5b6c7"}],"tags":["Webhooks"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointCreateRequest"},"examples":{"example-1":{"value":{"url":"https://partner.example.com/hooks/hi-doctor","events":["consultation.approved","consultation.rejected","prescription.issued"],"description":"Order system"},"summary":"Illustrative request example"}}}},"required":true},"security":[{"PartnerApiKey":[]}],"responses":{"201":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointWithSecret"},"examples":{"example-1":{"value":{"id":"1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","object":"webhook_endpoint","url":"https://partner.example.com/hooks/hi-doctor","description":"Order system","mode":"test","events":["consultation.approved","consultation.rejected","prescription.issued"],"status":"enabled","failure_streak":0,"last_success_at":null,"last_failure_at":null,"previous_secret_expires_at":null,"created_at":"2026-11-03T10:15:00Z","updated_at":"2026-11-03T10:15:00Z","secret":"whsec_Q2hhbmdlTWVJbkFSZWFsU2VjcmV0U3RvcmUxMjM0NTY="},"summary":"Illustrative response example"}}}},"description":"The new endpoint and its secret."},"400":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"invalid_request","code":"parse_error","message":"JSON parse error - Expecting value: line 1 column 1 (char 0)","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"invalid_request","code":"brand_header_not_allowed","message":"Brand headers are not accepted on partner requests; the brand comes from the API key.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request is malformed. Partner routes also refuse the patient-app brand header with 400 brand_header_not_allowed, before authentication (no rate-limit headers)."},"401":{"headers":{"WWW-Authenticate":{"schema":{"type":"string"},"description":"The authentication scheme to use: Bearer realm=\"hi-doctor-partner\"."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"authentication_error","code":"not_authenticated","message":"Authentication credentials were not provided.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"authentication_error","code":"invalid_token","message":"The API key is not valid.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"authentication_error","code":"key_revoked","message":"The API key has been revoked.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"authentication_error","code":"key_expired","message":"The API key has expired.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"authentication_error","code":"partner_suspended","message":"The partner account is suspended.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The API key is missing, malformed, unknown, revoked or expired, or the partner account is suspended. The response carries WWW-Authenticate: Bearer realm=\"hi-doctor-partner\" and no rate-limit headers."},"403":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"permission_error","code":"insufficient_scope","message":"This endpoint requires the consultations:read, prescriptions:read, webhooks:manage scope.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"permission_error","code":"partner_not_live","message":"Live mode is not enabled for this partner.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The key lacks the consultations:read, prescriptions:read, webhooks:manage scopes (insufficient_scope), or a live key was used while live mode is not enabled for the partner (partner_not_live)."},"404":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PartnerError"},{"$ref":"#/components/schemas/PartnerFlagOffError"}]},"examples":{"example-1":{"value":{"error":{"type":"not_found"}},"summary":"Illustrative response example"}}}},"description":"While the Partner API is switched off for the environment, every route answers 404 with only {\"error\": {\"type\": \"not_found\"}}, before authentication."},"422":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"validation_error","code":"invalid_request","message":"The request body is invalid.","errors":[{"field":"url","code":"required","message":"This field is required."}],"request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"validation_error","code":"invalid_webhook_url","message":"The URL must be https on port 443 without credentials.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"validation_error","code":"invalid_webhook_url","message":"The URL must point to a public address.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"validation_error","code":"invalid_webhook_events","message":"events must be [\"*\"] or a non-empty subset of ['consultation.approved', 'consultation.cancelled', 'consultation.in_review', 'consultation.info_requested', 'consultation.rejected', 'consultation.submitted', 'consultation.updated', 'prescription.cancelled', 'prescription.issued', 'prescription.replaced'].","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"validation_error","code":"mode_mismatch","message":"A key manages endpoints in its own mode only.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request body is invalid."},"429":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"rate_limited","code":"throttled","message":"Request was throttled. Expected available in 12 seconds.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"Too many requests; wait for the number of seconds in Retry-After."},"503":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"api_error","code":"webhooks_unavailable","message":"Webhooks are not available right now.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"Webhooks are not available right now (the signing-secret store is unavailable). Retry later."}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"set -eu\numask 077\n# Path values must be URL-encoded; query string excludes the leading question mark.\nurl=\"${HD_BASE_URL:?Set API origin}/v1/partner/webhook-endpoints/${HD_QUERY_STRING:+?${HD_QUERY_STRING}}\"\nprintf 'Accept: application/json\\n' > \"${HD_REQUEST_HEADERS_FILE:?Private header file}\"\nprintf 'Authorization: Bearer %s\\n' \"${HD_PARTNER_API_KEY:?Set partner API key}\" >> \"$HD_REQUEST_HEADERS_FILE\"\ncurl --silent --show-error --max-time 30 --request POST \\\n  --header \"@$HD_REQUEST_HEADERS_FILE\" \\\n  --dump-header \"${HD_HEADERS_FILE:?Private response header file}\" \\\n  --output \"${HD_RESPONSE_FILE:?Private response file}\" \\\n  --write-out '%{http_code}\\n%{content_type}\\n' \\\n  --header 'Content-Type: application/json' \\\n  --data-binary \"@${HD_REQUEST_FILE:?JSON fixture file}\" \\\n  \"$url\"\n"},{"lang":"Python","label":"requests","source":"import os, json\nfrom pathlib import Path\nfrom urllib.parse import quote\nimport requests\nos.umask(0o077)\nurl = os.environ[\"HD_BASE_URL\"].rstrip(\"/\") + '/v1/partner/webhook-endpoints/'\nif os.environ.get(\"HD_QUERY_STRING\"): url += \"?\" + os.environ[\"HD_QUERY_STRING\"]\nheaders = {\"Accept\": 'application/json'}\nheaders[\"Authorization\"] = \"Bearer \" + os.environ[\"HD_PARTNER_API_KEY\"]\nkwargs = {}\nkwargs[\"json\"] = json.loads(Path(os.environ[\"HD_REQUEST_FILE\"]).read_text())\nresponse = requests.request('POST', url, headers=headers, timeout=30, allow_redirects=False, **kwargs)\nPath(os.environ[\"HD_RESPONSE_FILE\"]).write_bytes(response.content)\nPath(os.environ[\"HD_HEADERS_FILE\"]).write_text(json.dumps(dict(response.headers)))\nprint(response.status_code)\nprint(response.headers.get(\"Content-Type\", \"\"))\n"},{"lang":"JavaScript","label":"fetch","source":"import { readFileSync, writeFileSync } from \"node:fs\";\nconst env = process.env;\nlet url = env.HD_BASE_URL.replace(/\\/$/, \"\") + \"/v1/partner/webhook-endpoints/\";\nif (env.HD_QUERY_STRING) url += \"?\" + env.HD_QUERY_STRING;\nconst headers = { \"Accept\": \"application/json\" };\nheaders.Authorization = \"Bearer \" + env.HD_PARTNER_API_KEY;\nconst options = { method: \"POST\", headers, redirect: \"manual\", signal: AbortSignal.timeout(30000) };\nheaders[\"Content-Type\"] = \"application/json\";\noptions.body = readFileSync(env.HD_REQUEST_FILE, \"utf8\");\nconst response = await fetch(url, options);\nwriteFileSync(env.HD_RESPONSE_FILE, Buffer.from(await response.arrayBuffer()), {mode: 0o600});\nwriteFileSync(env.HD_HEADERS_FILE, JSON.stringify(Object.fromEntries(response.headers)), {mode: 0o600});\nconsole.log(response.status);\nconsole.log(response.headers.get(\"content-type\") ?? \"\");\n"}]}},"/v1/partner/webhook-endpoints/{endpoint_id}/":{"get":{"operationId":"get_webhook_endpoint","description":"One webhook endpoint and its delivery health. Requires the consultations:read, prescriptions:read and webhooks:manage scopes.\n\n### Access, effects and retries\n**Ownership:** The API key's own partner and mode (test or live); another partner's or mode's records answer 404.\n\n**Prerequisites:** A partner API key with the `webhooks:manage` and `consultations:read` and `prescriptions:read` scopes.\n\n**Effects:** None.\n\n**Retries:** Safe to retry.","summary":"Get a webhook endpoint","parameters":[{"in":"path","name":"endpoint_id","schema":{"type":"string","format":"uuid"},"description":"The webhook endpoint id.","required":true,"example":"3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f"}],"tags":["Webhooks"],"security":[{"PartnerApiKey":[]}],"responses":{"200":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpoint"},"examples":{"example-1":{"value":{"id":"1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","object":"webhook_endpoint","url":"https://partner.example.com/hooks/hi-doctor","description":"Order system","mode":"test","events":["consultation.approved","consultation.rejected","prescription.issued"],"status":"enabled","failure_streak":0,"last_success_at":"2026-11-03T12:40:00Z","last_failure_at":null,"previous_secret_expires_at":null,"created_at":"2026-11-03T10:15:00Z","updated_at":"2026-11-03T10:15:00Z"},"summary":"Illustrative response example"}}}},"description":"Success."},"400":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"invalid_request","code":"brand_header_not_allowed","message":"Brand headers are not accepted on partner requests; the brand comes from the API key.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request is malformed. Partner routes also refuse the patient-app brand header with 400 brand_header_not_allowed, before authentication (no rate-limit headers)."},"401":{"headers":{"WWW-Authenticate":{"schema":{"type":"string"},"description":"The authentication scheme to use: Bearer realm=\"hi-doctor-partner\"."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"authentication_error","code":"not_authenticated","message":"Authentication credentials were not provided.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"authentication_error","code":"invalid_token","message":"The API key is not valid.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"authentication_error","code":"key_revoked","message":"The API key has been revoked.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"authentication_error","code":"key_expired","message":"The API key has expired.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"authentication_error","code":"partner_suspended","message":"The partner account is suspended.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The API key is missing, malformed, unknown, revoked or expired, or the partner account is suspended. The response carries WWW-Authenticate: Bearer realm=\"hi-doctor-partner\" and no rate-limit headers."},"403":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"permission_error","code":"insufficient_scope","message":"This endpoint requires the consultations:read, prescriptions:read, webhooks:manage scope.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"permission_error","code":"partner_not_live","message":"Live mode is not enabled for this partner.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The key lacks the consultations:read, prescriptions:read, webhooks:manage scopes (insufficient_scope), or a live key was used while live mode is not enabled for the partner (partner_not_live)."},"404":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PartnerError"},{"$ref":"#/components/schemas/PartnerFlagOffError"}]},"examples":{"example-1":{"value":{"error":{"type":"not_found","code":"not_found","message":"No such resource.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"not_found"}},"summary":"Illustrative response example"}}}},"description":"No such resource for this key's partner and mode. While the Partner API is switched off for the environment, every route answers 404 with only {\"error\": {\"type\": \"not_found\"}}, before authentication."},"429":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"rate_limited","code":"throttled","message":"Request was throttled. Expected available in 12 seconds.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"Too many requests; wait for the number of seconds in Retry-After."}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"set -eu\numask 077\n# Path values must be URL-encoded; query string excludes the leading question mark.\nurl=\"${HD_BASE_URL:?Set API origin}/v1/partner/webhook-endpoints/${HD_PATH_ENDPOINT_ID:?Set the path fixture}/${HD_QUERY_STRING:+?${HD_QUERY_STRING}}\"\nprintf 'Accept: application/json\\n' > \"${HD_REQUEST_HEADERS_FILE:?Private header file}\"\nprintf 'Authorization: Bearer %s\\n' \"${HD_PARTNER_API_KEY:?Set partner API key}\" >> \"$HD_REQUEST_HEADERS_FILE\"\ncurl --silent --show-error --max-time 30 --request GET \\\n  --header \"@$HD_REQUEST_HEADERS_FILE\" \\\n  --dump-header \"${HD_HEADERS_FILE:?Private response header file}\" \\\n  --output \"${HD_RESPONSE_FILE:?Private response file}\" \\\n  --write-out '%{http_code}\\n%{content_type}\\n' \\\n  \"$url\"\n"},{"lang":"Python","label":"requests","source":"import os, json\nfrom pathlib import Path\nfrom urllib.parse import quote\nimport requests\nos.umask(0o077)\nurl = os.environ[\"HD_BASE_URL\"].rstrip(\"/\") + '/v1/partner/webhook-endpoints/{endpoint_id}/'\nurl = url.replace('{endpoint_id}', quote(os.environ['HD_PATH_ENDPOINT_ID'], safe=\"\"))\nif os.environ.get(\"HD_QUERY_STRING\"): url += \"?\" + os.environ[\"HD_QUERY_STRING\"]\nheaders = {\"Accept\": 'application/json'}\nheaders[\"Authorization\"] = \"Bearer \" + os.environ[\"HD_PARTNER_API_KEY\"]\nkwargs = {}\nresponse = requests.request('GET', url, headers=headers, timeout=30, allow_redirects=False, **kwargs)\nPath(os.environ[\"HD_RESPONSE_FILE\"]).write_bytes(response.content)\nPath(os.environ[\"HD_HEADERS_FILE\"]).write_text(json.dumps(dict(response.headers)))\nprint(response.status_code)\nprint(response.headers.get(\"Content-Type\", \"\"))\n"},{"lang":"JavaScript","label":"fetch","source":"import { readFileSync, writeFileSync } from \"node:fs\";\nconst env = process.env;\nlet url = env.HD_BASE_URL.replace(/\\/$/, \"\") + \"/v1/partner/webhook-endpoints/{endpoint_id}/\";\nurl = url.replace(\"{endpoint_id}\", encodeURIComponent(env.HD_PATH_ENDPOINT_ID));\nif (env.HD_QUERY_STRING) url += \"?\" + env.HD_QUERY_STRING;\nconst headers = { \"Accept\": \"application/json\" };\nheaders.Authorization = \"Bearer \" + env.HD_PARTNER_API_KEY;\nconst options = { method: \"GET\", headers, redirect: \"manual\", signal: AbortSignal.timeout(30000) };\nconst response = await fetch(url, options);\nwriteFileSync(env.HD_RESPONSE_FILE, Buffer.from(await response.arrayBuffer()), {mode: 0o600});\nwriteFileSync(env.HD_HEADERS_FILE, JSON.stringify(Object.fromEntries(response.headers)), {mode: 0o600});\nconsole.log(response.status);\nconsole.log(response.headers.get(\"content-type\") ?? \"\");\n"}]},"patch":{"operationId":"update_webhook_endpoint","description":"Changes the URL, events or description, or enables or disables the endpoint. Re-enabling resets the failure streak. Requires the consultations:read, prescriptions:read and webhooks:manage scopes.\n\n### Access, effects and retries\n**Ownership:** The API key's own partner and mode (test or live); another partner's or mode's records answer 404.\n\n**Prerequisites:** A partner API key with the `webhooks:manage` and `consultations:read` and `prescriptions:read` scopes.\n\n**Effects:** Changes the endpoint; re-enabling resets its failure streak.\n\n**Retries:** Safe to retry with the same body.","summary":"Update a webhook endpoint","parameters":[{"in":"path","name":"endpoint_id","schema":{"type":"string","format":"uuid"},"description":"The webhook endpoint id.","required":true,"example":"3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f"}],"tags":["Webhooks"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchedWebhookEndpointUpdateRequest"},"examples":{"example-1":{"value":{"status":"disabled"},"summary":"Illustrative request example"}}}}},"security":[{"PartnerApiKey":[]}],"responses":{"200":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpoint"},"examples":{"example-1":{"value":{"id":"1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","object":"webhook_endpoint","url":"https://partner.example.com/hooks/hi-doctor","description":"Order system","mode":"test","events":["consultation.approved","consultation.rejected","prescription.issued"],"status":"disabled","failure_streak":0,"last_success_at":"2026-11-03T12:40:00Z","last_failure_at":null,"previous_secret_expires_at":null,"created_at":"2026-11-03T10:15:00Z","updated_at":"2026-11-03T12:40:00Z"},"summary":"Illustrative response example"}}}},"description":"Success."},"400":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"invalid_request","code":"parse_error","message":"JSON parse error - Expecting value: line 1 column 1 (char 0)","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"invalid_request","code":"brand_header_not_allowed","message":"Brand headers are not accepted on partner requests; the brand comes from the API key.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request is malformed. Partner routes also refuse the patient-app brand header with 400 brand_header_not_allowed, before authentication (no rate-limit headers)."},"401":{"headers":{"WWW-Authenticate":{"schema":{"type":"string"},"description":"The authentication scheme to use: Bearer realm=\"hi-doctor-partner\"."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"authentication_error","code":"not_authenticated","message":"Authentication credentials were not provided.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"authentication_error","code":"invalid_token","message":"The API key is not valid.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"authentication_error","code":"key_revoked","message":"The API key has been revoked.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"authentication_error","code":"key_expired","message":"The API key has expired.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"authentication_error","code":"partner_suspended","message":"The partner account is suspended.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The API key is missing, malformed, unknown, revoked or expired, or the partner account is suspended. The response carries WWW-Authenticate: Bearer realm=\"hi-doctor-partner\" and no rate-limit headers."},"403":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"permission_error","code":"insufficient_scope","message":"This endpoint requires the consultations:read, prescriptions:read, webhooks:manage scope.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"permission_error","code":"partner_not_live","message":"Live mode is not enabled for this partner.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The key lacks the consultations:read, prescriptions:read, webhooks:manage scopes (insufficient_scope), or a live key was used while live mode is not enabled for the partner (partner_not_live)."},"404":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PartnerError"},{"$ref":"#/components/schemas/PartnerFlagOffError"}]},"examples":{"example-1":{"value":{"error":{"type":"not_found","code":"not_found","message":"No such resource.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"not_found"}},"summary":"Illustrative response example"}}}},"description":"No such resource for this key's partner and mode. While the Partner API is switched off for the environment, every route answers 404 with only {\"error\": {\"type\": \"not_found\"}}, before authentication."},"422":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"validation_error","code":"invalid_request","message":"The request body is invalid.","errors":[{"field":"status","code":"invalid_choice","message":"This is not a valid choice."}],"request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"validation_error","code":"invalid_webhook_url","message":"The URL must be https on port 443 without credentials.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"validation_error","code":"invalid_webhook_url","message":"The URL must point to a public address.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"validation_error","code":"invalid_webhook_events","message":"events must be [\"*\"] or a non-empty subset of ['consultation.approved', 'consultation.cancelled', 'consultation.in_review', 'consultation.info_requested', 'consultation.rejected', 'consultation.submitted', 'consultation.updated', 'prescription.cancelled', 'prescription.issued', 'prescription.replaced'].","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request body is invalid."},"429":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"rate_limited","code":"throttled","message":"Request was throttled. Expected available in 12 seconds.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"Too many requests; wait for the number of seconds in Retry-After."},"503":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"api_error","code":"webhooks_unavailable","message":"Webhooks are not available right now.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"Webhooks are not available right now (the signing-secret store is unavailable). Retry later."}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"set -eu\numask 077\n# Path values must be URL-encoded; query string excludes the leading question mark.\nurl=\"${HD_BASE_URL:?Set API origin}/v1/partner/webhook-endpoints/${HD_PATH_ENDPOINT_ID:?Set the path fixture}/${HD_QUERY_STRING:+?${HD_QUERY_STRING}}\"\nprintf 'Accept: application/json\\n' > \"${HD_REQUEST_HEADERS_FILE:?Private header file}\"\nprintf 'Authorization: Bearer %s\\n' \"${HD_PARTNER_API_KEY:?Set partner API key}\" >> \"$HD_REQUEST_HEADERS_FILE\"\ncurl --silent --show-error --max-time 30 --request PATCH \\\n  --header \"@$HD_REQUEST_HEADERS_FILE\" \\\n  --dump-header \"${HD_HEADERS_FILE:?Private response header file}\" \\\n  --output \"${HD_RESPONSE_FILE:?Private response file}\" \\\n  --write-out '%{http_code}\\n%{content_type}\\n' \\\n  --header 'Content-Type: application/json' \\\n  --data-binary \"@${HD_REQUEST_FILE:?JSON fixture file}\" \\\n  \"$url\"\n"},{"lang":"Python","label":"requests","source":"import os, json\nfrom pathlib import Path\nfrom urllib.parse import quote\nimport requests\nos.umask(0o077)\nurl = os.environ[\"HD_BASE_URL\"].rstrip(\"/\") + '/v1/partner/webhook-endpoints/{endpoint_id}/'\nurl = url.replace('{endpoint_id}', quote(os.environ['HD_PATH_ENDPOINT_ID'], safe=\"\"))\nif os.environ.get(\"HD_QUERY_STRING\"): url += \"?\" + os.environ[\"HD_QUERY_STRING\"]\nheaders = {\"Accept\": 'application/json'}\nheaders[\"Authorization\"] = \"Bearer \" + os.environ[\"HD_PARTNER_API_KEY\"]\nkwargs = {}\nkwargs[\"json\"] = json.loads(Path(os.environ[\"HD_REQUEST_FILE\"]).read_text())\nresponse = requests.request('PATCH', url, headers=headers, timeout=30, allow_redirects=False, **kwargs)\nPath(os.environ[\"HD_RESPONSE_FILE\"]).write_bytes(response.content)\nPath(os.environ[\"HD_HEADERS_FILE\"]).write_text(json.dumps(dict(response.headers)))\nprint(response.status_code)\nprint(response.headers.get(\"Content-Type\", \"\"))\n"},{"lang":"JavaScript","label":"fetch","source":"import { readFileSync, writeFileSync } from \"node:fs\";\nconst env = process.env;\nlet url = env.HD_BASE_URL.replace(/\\/$/, \"\") + \"/v1/partner/webhook-endpoints/{endpoint_id}/\";\nurl = url.replace(\"{endpoint_id}\", encodeURIComponent(env.HD_PATH_ENDPOINT_ID));\nif (env.HD_QUERY_STRING) url += \"?\" + env.HD_QUERY_STRING;\nconst headers = { \"Accept\": \"application/json\" };\nheaders.Authorization = \"Bearer \" + env.HD_PARTNER_API_KEY;\nconst options = { method: \"PATCH\", headers, redirect: \"manual\", signal: AbortSignal.timeout(30000) };\nheaders[\"Content-Type\"] = \"application/json\";\noptions.body = readFileSync(env.HD_REQUEST_FILE, \"utf8\");\nconst response = await fetch(url, options);\nwriteFileSync(env.HD_RESPONSE_FILE, Buffer.from(await response.arrayBuffer()), {mode: 0o600});\nwriteFileSync(env.HD_HEADERS_FILE, JSON.stringify(Object.fromEntries(response.headers)), {mode: 0o600});\nconsole.log(response.status);\nconsole.log(response.headers.get(\"content-type\") ?? \"\");\n"}]},"delete":{"operationId":"delete_webhook_endpoint","description":"Deletes the endpoint and its deliveries. Requires the consultations:read, prescriptions:read and webhooks:manage scopes.\n\n### Access, effects and retries\n**Ownership:** The API key's own partner and mode (test or live); another partner's or mode's records answer 404.\n\n**Prerequisites:** A partner API key with the `webhooks:manage` and `consultations:read` and `prescriptions:read` scopes.\n\n**Effects:** Deletes the endpoint and its pending deliveries.\n\n**Retries:** A retry after a successful delete answers 404.","summary":"Delete a webhook endpoint","parameters":[{"in":"path","name":"endpoint_id","schema":{"type":"string","format":"uuid"},"description":"The webhook endpoint id.","required":true,"example":"3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f"}],"tags":["Webhooks"],"security":[{"PartnerApiKey":[]}],"responses":{"204":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"description":"Deleted."},"400":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"invalid_request","code":"brand_header_not_allowed","message":"Brand headers are not accepted on partner requests; the brand comes from the API key.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request is malformed. Partner routes also refuse the patient-app brand header with 400 brand_header_not_allowed, before authentication (no rate-limit headers)."},"401":{"headers":{"WWW-Authenticate":{"schema":{"type":"string"},"description":"The authentication scheme to use: Bearer realm=\"hi-doctor-partner\"."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"authentication_error","code":"not_authenticated","message":"Authentication credentials were not provided.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"authentication_error","code":"invalid_token","message":"The API key is not valid.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"authentication_error","code":"key_revoked","message":"The API key has been revoked.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"authentication_error","code":"key_expired","message":"The API key has expired.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"authentication_error","code":"partner_suspended","message":"The partner account is suspended.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The API key is missing, malformed, unknown, revoked or expired, or the partner account is suspended. The response carries WWW-Authenticate: Bearer realm=\"hi-doctor-partner\" and no rate-limit headers."},"403":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"permission_error","code":"insufficient_scope","message":"This endpoint requires the consultations:read, prescriptions:read, webhooks:manage scope.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"permission_error","code":"partner_not_live","message":"Live mode is not enabled for this partner.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The key lacks the consultations:read, prescriptions:read, webhooks:manage scopes (insufficient_scope), or a live key was used while live mode is not enabled for the partner (partner_not_live)."},"404":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PartnerError"},{"$ref":"#/components/schemas/PartnerFlagOffError"}]},"examples":{"example-1":{"value":{"error":{"type":"not_found","code":"not_found","message":"No such resource.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"not_found"}},"summary":"Illustrative response example"}}}},"description":"No such resource for this key's partner and mode. While the Partner API is switched off for the environment, every route answers 404 with only {\"error\": {\"type\": \"not_found\"}}, before authentication."},"429":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"rate_limited","code":"throttled","message":"Request was throttled. Expected available in 12 seconds.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"Too many requests; wait for the number of seconds in Retry-After."}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"set -eu\numask 077\n# Path values must be URL-encoded; query string excludes the leading question mark.\nurl=\"${HD_BASE_URL:?Set API origin}/v1/partner/webhook-endpoints/${HD_PATH_ENDPOINT_ID:?Set the path fixture}/${HD_QUERY_STRING:+?${HD_QUERY_STRING}}\"\nprintf 'Accept: application/json\\n' > \"${HD_REQUEST_HEADERS_FILE:?Private header file}\"\nprintf 'Authorization: Bearer %s\\n' \"${HD_PARTNER_API_KEY:?Set partner API key}\" >> \"$HD_REQUEST_HEADERS_FILE\"\ncurl --silent --show-error --max-time 30 --request DELETE \\\n  --header \"@$HD_REQUEST_HEADERS_FILE\" \\\n  --dump-header \"${HD_HEADERS_FILE:?Private response header file}\" \\\n  --output \"${HD_RESPONSE_FILE:?Private response file}\" \\\n  --write-out '%{http_code}\\n%{content_type}\\n' \\\n  \"$url\"\n"},{"lang":"Python","label":"requests","source":"import os, json\nfrom pathlib import Path\nfrom urllib.parse import quote\nimport requests\nos.umask(0o077)\nurl = os.environ[\"HD_BASE_URL\"].rstrip(\"/\") + '/v1/partner/webhook-endpoints/{endpoint_id}/'\nurl = url.replace('{endpoint_id}', quote(os.environ['HD_PATH_ENDPOINT_ID'], safe=\"\"))\nif os.environ.get(\"HD_QUERY_STRING\"): url += \"?\" + os.environ[\"HD_QUERY_STRING\"]\nheaders = {\"Accept\": 'application/json'}\nheaders[\"Authorization\"] = \"Bearer \" + os.environ[\"HD_PARTNER_API_KEY\"]\nkwargs = {}\nresponse = requests.request('DELETE', url, headers=headers, timeout=30, allow_redirects=False, **kwargs)\nPath(os.environ[\"HD_RESPONSE_FILE\"]).write_bytes(response.content)\nPath(os.environ[\"HD_HEADERS_FILE\"]).write_text(json.dumps(dict(response.headers)))\nprint(response.status_code)\nprint(response.headers.get(\"Content-Type\", \"\"))\n"},{"lang":"JavaScript","label":"fetch","source":"import { readFileSync, writeFileSync } from \"node:fs\";\nconst env = process.env;\nlet url = env.HD_BASE_URL.replace(/\\/$/, \"\") + \"/v1/partner/webhook-endpoints/{endpoint_id}/\";\nurl = url.replace(\"{endpoint_id}\", encodeURIComponent(env.HD_PATH_ENDPOINT_ID));\nif (env.HD_QUERY_STRING) url += \"?\" + env.HD_QUERY_STRING;\nconst headers = { \"Accept\": \"application/json\" };\nheaders.Authorization = \"Bearer \" + env.HD_PARTNER_API_KEY;\nconst options = { method: \"DELETE\", headers, redirect: \"manual\", signal: AbortSignal.timeout(30000) };\nconst response = await fetch(url, options);\nwriteFileSync(env.HD_RESPONSE_FILE, Buffer.from(await response.arrayBuffer()), {mode: 0o600});\nwriteFileSync(env.HD_HEADERS_FILE, JSON.stringify(Object.fromEntries(response.headers)), {mode: 0o600});\nconsole.log(response.status);\nconsole.log(response.headers.get(\"content-type\") ?? \"\");\n"}]}},"/v1/partner/webhook-endpoints/{endpoint_id}/rotate-secret/":{"post":{"operationId":"rotate_webhook_endpoint_secret","description":"Issues a new signing secret, returned once. Deliveries are signed with both the new and the previous secret for 24 hours so you can switch over. Requires the consultations:read, prescriptions:read and webhooks:manage scopes.\n\n### Access, effects and retries\n**Ownership:** The API key's own partner and mode (test or live); another partner's or mode's records answer 404.\n\n**Prerequisites:** A partner API key with the `webhooks:manage` and `consultations:read` and `prescriptions:read` scopes.\n\n**Effects:** Issues a new signing secret; the previous one keeps signing for 24 hours.\n\n**Retries:** Each call issues a new secret; keep the last one returned.","summary":"Rotate a webhook signing secret","parameters":[{"in":"path","name":"endpoint_id","schema":{"type":"string","format":"uuid"},"description":"The webhook endpoint id.","required":true,"example":"3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f"},{"in":"header","name":"Idempotency-Key","schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{8,128}$"},"description":"Accepted but not used for replay on this operation. Only POST /v1/partner/consultations stores and replays responses by Idempotency-Key.","example":"b7c1d2e3f4a5b6c7"}],"tags":["Webhooks"],"security":[{"PartnerApiKey":[]}],"responses":{"200":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointWithSecret"},"examples":{"example-1":{"value":{"id":"1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","object":"webhook_endpoint","url":"https://partner.example.com/hooks/hi-doctor","description":"Order system","mode":"test","events":["consultation.approved","consultation.rejected","prescription.issued"],"status":"enabled","failure_streak":0,"last_success_at":"2026-11-03T12:40:00Z","last_failure_at":null,"previous_secret_expires_at":"2026-11-04T12:40:00Z","created_at":"2026-11-03T10:15:00Z","updated_at":"2026-11-03T12:40:00Z","secret":"whsec_Q2hhbmdlTWVJbkFSZWFsU2VjcmV0U3RvcmUxMjM0NTY="},"summary":"Illustrative response example"}}}},"description":"The endpoint and its new secret."},"400":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"invalid_request","code":"brand_header_not_allowed","message":"Brand headers are not accepted on partner requests; the brand comes from the API key.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request is malformed. Partner routes also refuse the patient-app brand header with 400 brand_header_not_allowed, before authentication (no rate-limit headers)."},"401":{"headers":{"WWW-Authenticate":{"schema":{"type":"string"},"description":"The authentication scheme to use: Bearer realm=\"hi-doctor-partner\"."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"authentication_error","code":"not_authenticated","message":"Authentication credentials were not provided.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"authentication_error","code":"invalid_token","message":"The API key is not valid.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"authentication_error","code":"key_revoked","message":"The API key has been revoked.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"authentication_error","code":"key_expired","message":"The API key has expired.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"authentication_error","code":"partner_suspended","message":"The partner account is suspended.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The API key is missing, malformed, unknown, revoked or expired, or the partner account is suspended. The response carries WWW-Authenticate: Bearer realm=\"hi-doctor-partner\" and no rate-limit headers."},"403":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"permission_error","code":"insufficient_scope","message":"This endpoint requires the consultations:read, prescriptions:read, webhooks:manage scope.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"permission_error","code":"partner_not_live","message":"Live mode is not enabled for this partner.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The key lacks the consultations:read, prescriptions:read, webhooks:manage scopes (insufficient_scope), or a live key was used while live mode is not enabled for the partner (partner_not_live)."},"404":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PartnerError"},{"$ref":"#/components/schemas/PartnerFlagOffError"}]},"examples":{"example-1":{"value":{"error":{"type":"not_found","code":"not_found","message":"No such resource.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"not_found"}},"summary":"Illustrative response example"}}}},"description":"No such resource for this key's partner and mode. While the Partner API is switched off for the environment, every route answers 404 with only {\"error\": {\"type\": \"not_found\"}}, before authentication."},"429":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"rate_limited","code":"throttled","message":"Request was throttled. Expected available in 12 seconds.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"Too many requests; wait for the number of seconds in Retry-After."},"503":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"api_error","code":"webhooks_unavailable","message":"Webhooks are not available right now.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"Webhooks are not available right now (the signing-secret store is unavailable). Retry later."}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"set -eu\numask 077\n# Path values must be URL-encoded; query string excludes the leading question mark.\nurl=\"${HD_BASE_URL:?Set API origin}/v1/partner/webhook-endpoints/${HD_PATH_ENDPOINT_ID:?Set the path fixture}/rotate-secret/${HD_QUERY_STRING:+?${HD_QUERY_STRING}}\"\nprintf 'Accept: application/json\\n' > \"${HD_REQUEST_HEADERS_FILE:?Private header file}\"\nprintf 'Authorization: Bearer %s\\n' \"${HD_PARTNER_API_KEY:?Set partner API key}\" >> \"$HD_REQUEST_HEADERS_FILE\"\ncurl --silent --show-error --max-time 30 --request POST \\\n  --header \"@$HD_REQUEST_HEADERS_FILE\" \\\n  --dump-header \"${HD_HEADERS_FILE:?Private response header file}\" \\\n  --output \"${HD_RESPONSE_FILE:?Private response file}\" \\\n  --write-out '%{http_code}\\n%{content_type}\\n' \\\n  \"$url\"\n"},{"lang":"Python","label":"requests","source":"import os, json\nfrom pathlib import Path\nfrom urllib.parse import quote\nimport requests\nos.umask(0o077)\nurl = os.environ[\"HD_BASE_URL\"].rstrip(\"/\") + '/v1/partner/webhook-endpoints/{endpoint_id}/rotate-secret/'\nurl = url.replace('{endpoint_id}', quote(os.environ['HD_PATH_ENDPOINT_ID'], safe=\"\"))\nif os.environ.get(\"HD_QUERY_STRING\"): url += \"?\" + os.environ[\"HD_QUERY_STRING\"]\nheaders = {\"Accept\": 'application/json'}\nheaders[\"Authorization\"] = \"Bearer \" + os.environ[\"HD_PARTNER_API_KEY\"]\nkwargs = {}\nresponse = requests.request('POST', url, headers=headers, timeout=30, allow_redirects=False, **kwargs)\nPath(os.environ[\"HD_RESPONSE_FILE\"]).write_bytes(response.content)\nPath(os.environ[\"HD_HEADERS_FILE\"]).write_text(json.dumps(dict(response.headers)))\nprint(response.status_code)\nprint(response.headers.get(\"Content-Type\", \"\"))\n"},{"lang":"JavaScript","label":"fetch","source":"import { readFileSync, writeFileSync } from \"node:fs\";\nconst env = process.env;\nlet url = env.HD_BASE_URL.replace(/\\/$/, \"\") + \"/v1/partner/webhook-endpoints/{endpoint_id}/rotate-secret/\";\nurl = url.replace(\"{endpoint_id}\", encodeURIComponent(env.HD_PATH_ENDPOINT_ID));\nif (env.HD_QUERY_STRING) url += \"?\" + env.HD_QUERY_STRING;\nconst headers = { \"Accept\": \"application/json\" };\nheaders.Authorization = \"Bearer \" + env.HD_PARTNER_API_KEY;\nconst options = { method: \"POST\", headers, redirect: \"manual\", signal: AbortSignal.timeout(30000) };\nconst response = await fetch(url, options);\nwriteFileSync(env.HD_RESPONSE_FILE, Buffer.from(await response.arrayBuffer()), {mode: 0o600});\nwriteFileSync(env.HD_HEADERS_FILE, JSON.stringify(Object.fromEntries(response.headers)), {mode: 0o600});\nconsole.log(response.status);\nconsole.log(response.headers.get(\"content-type\") ?? \"\");\n"}]}},"/v1/partner/webhook-endpoints/{endpoint_id}/test/":{"post":{"operationId":"send_webhook_test_event","description":"Queues an endpoint.test event to this endpoint only. The endpoint must be enabled. Requires the consultations:read, prescriptions:read and webhooks:manage scopes.\n\n### Access, effects and retries\n**Ownership:** The API key's own partner and mode (test or live); another partner's or mode's records answer 404.\n\n**Prerequisites:** A partner API key with the `webhooks:manage` and `consultations:read` and `prescriptions:read` scopes.\n\n**Effects:** Queues one endpoint.test delivery to this endpoint.\n\n**Retries:** Each call queues another test delivery.","summary":"Send a test event","parameters":[{"in":"path","name":"endpoint_id","schema":{"type":"string","format":"uuid"},"description":"The webhook endpoint id.","required":true,"example":"3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f"},{"in":"header","name":"Idempotency-Key","schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{8,128}$"},"description":"Accepted but not used for replay on this operation. Only POST /v1/partner/consultations stores and replays responses by Idempotency-Key.","example":"b7c1d2e3f4a5b6c7"}],"tags":["Webhooks"],"security":[{"PartnerApiKey":[]}],"responses":{"202":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDelivery"},"examples":{"example-1":{"value":{"id":"6e5d4c3b-2a19-4807-b6a5-948372615049","object":"webhook_delivery","endpoint_id":"1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","event_id":"2b3c4d5e-6f70-4182-93a4-b5c6d7e8f901","event_type":"endpoint.test","status":"pending","attempt_count":0,"next_attempt_at":"2026-11-03T12:40:00Z","last_attempt_at":null,"delivered_at":null,"response_status":null,"response_excerpt":"","error":"","created_at":"2026-11-03T12:40:00Z"},"summary":"Illustrative response example"}}}},"description":"The queued delivery."},"400":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"invalid_request","code":"brand_header_not_allowed","message":"Brand headers are not accepted on partner requests; the brand comes from the API key.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request is malformed. Partner routes also refuse the patient-app brand header with 400 brand_header_not_allowed, before authentication (no rate-limit headers)."},"401":{"headers":{"WWW-Authenticate":{"schema":{"type":"string"},"description":"The authentication scheme to use: Bearer realm=\"hi-doctor-partner\"."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"authentication_error","code":"not_authenticated","message":"Authentication credentials were not provided.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"authentication_error","code":"invalid_token","message":"The API key is not valid.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"authentication_error","code":"key_revoked","message":"The API key has been revoked.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"authentication_error","code":"key_expired","message":"The API key has expired.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"authentication_error","code":"partner_suspended","message":"The partner account is suspended.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The API key is missing, malformed, unknown, revoked or expired, or the partner account is suspended. The response carries WWW-Authenticate: Bearer realm=\"hi-doctor-partner\" and no rate-limit headers."},"403":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"permission_error","code":"insufficient_scope","message":"This endpoint requires the consultations:read, prescriptions:read, webhooks:manage scope.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"permission_error","code":"partner_not_live","message":"Live mode is not enabled for this partner.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The key lacks the consultations:read, prescriptions:read, webhooks:manage scopes (insufficient_scope), or a live key was used while live mode is not enabled for the partner (partner_not_live)."},"404":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PartnerError"},{"$ref":"#/components/schemas/PartnerFlagOffError"}]},"examples":{"example-1":{"value":{"error":{"type":"not_found","code":"not_found","message":"No such resource.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"not_found"}},"summary":"Illustrative response example"}}}},"description":"No such resource for this key's partner and mode. While the Partner API is switched off for the environment, every route answers 404 with only {\"error\": {\"type\": \"not_found\"}}, before authentication."},"409":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"conflict","code":"endpoint_disabled","message":"Enable the endpoint before sending a test event.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request conflicts with the resource's current state."},"429":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"rate_limited","code":"throttled","message":"Request was throttled. Expected available in 12 seconds.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"Too many requests; wait for the number of seconds in Retry-After."}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"set -eu\numask 077\n# Path values must be URL-encoded; query string excludes the leading question mark.\nurl=\"${HD_BASE_URL:?Set API origin}/v1/partner/webhook-endpoints/${HD_PATH_ENDPOINT_ID:?Set the path fixture}/test/${HD_QUERY_STRING:+?${HD_QUERY_STRING}}\"\nprintf 'Accept: application/json\\n' > \"${HD_REQUEST_HEADERS_FILE:?Private header file}\"\nprintf 'Authorization: Bearer %s\\n' \"${HD_PARTNER_API_KEY:?Set partner API key}\" >> \"$HD_REQUEST_HEADERS_FILE\"\ncurl --silent --show-error --max-time 30 --request POST \\\n  --header \"@$HD_REQUEST_HEADERS_FILE\" \\\n  --dump-header \"${HD_HEADERS_FILE:?Private response header file}\" \\\n  --output \"${HD_RESPONSE_FILE:?Private response file}\" \\\n  --write-out '%{http_code}\\n%{content_type}\\n' \\\n  \"$url\"\n"},{"lang":"Python","label":"requests","source":"import os, json\nfrom pathlib import Path\nfrom urllib.parse import quote\nimport requests\nos.umask(0o077)\nurl = os.environ[\"HD_BASE_URL\"].rstrip(\"/\") + '/v1/partner/webhook-endpoints/{endpoint_id}/test/'\nurl = url.replace('{endpoint_id}', quote(os.environ['HD_PATH_ENDPOINT_ID'], safe=\"\"))\nif os.environ.get(\"HD_QUERY_STRING\"): url += \"?\" + os.environ[\"HD_QUERY_STRING\"]\nheaders = {\"Accept\": 'application/json'}\nheaders[\"Authorization\"] = \"Bearer \" + os.environ[\"HD_PARTNER_API_KEY\"]\nkwargs = {}\nresponse = requests.request('POST', url, headers=headers, timeout=30, allow_redirects=False, **kwargs)\nPath(os.environ[\"HD_RESPONSE_FILE\"]).write_bytes(response.content)\nPath(os.environ[\"HD_HEADERS_FILE\"]).write_text(json.dumps(dict(response.headers)))\nprint(response.status_code)\nprint(response.headers.get(\"Content-Type\", \"\"))\n"},{"lang":"JavaScript","label":"fetch","source":"import { readFileSync, writeFileSync } from \"node:fs\";\nconst env = process.env;\nlet url = env.HD_BASE_URL.replace(/\\/$/, \"\") + \"/v1/partner/webhook-endpoints/{endpoint_id}/test/\";\nurl = url.replace(\"{endpoint_id}\", encodeURIComponent(env.HD_PATH_ENDPOINT_ID));\nif (env.HD_QUERY_STRING) url += \"?\" + env.HD_QUERY_STRING;\nconst headers = { \"Accept\": \"application/json\" };\nheaders.Authorization = \"Bearer \" + env.HD_PARTNER_API_KEY;\nconst options = { method: \"POST\", headers, redirect: \"manual\", signal: AbortSignal.timeout(30000) };\nconst response = await fetch(url, options);\nwriteFileSync(env.HD_RESPONSE_FILE, Buffer.from(await response.arrayBuffer()), {mode: 0o600});\nwriteFileSync(env.HD_HEADERS_FILE, JSON.stringify(Object.fromEntries(response.headers)), {mode: 0o600});\nconsole.log(response.status);\nconsole.log(response.headers.get(\"content-type\") ?? \"\");\n"}]}},"/v1/partner/webhook-endpoints/{endpoint_id}/deliveries/":{"get":{"operationId":"list_webhook_deliveries","description":"Delivery attempts to this endpoint, newest first, with cursor pagination. A delivery is deleted about 30 days after it was created, unless it was replayed in that window or a worker is sending it. Requires the consultations:read, prescriptions:read and webhooks:manage scopes.\n\n### Access, effects and retries\n**Ownership:** The API key's own partner and mode (test or live); another partner's or mode's records answer 404.\n\n**Prerequisites:** A partner API key with the `webhooks:manage` and `consultations:read` and `prescriptions:read` scopes.\n\n**Effects:** None.\n\n**Retries:** Safe to retry.","summary":"List deliveries for an endpoint","parameters":[{"in":"path","name":"endpoint_id","schema":{"type":"string","format":"uuid"},"description":"The webhook endpoint id.","required":true,"example":"3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f"},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Page size, 1 to 100. Default 50. A value outside the range is refused with 400 invalid_request.","example":50},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"The next_cursor of the previous page.","example":"MjAyNi0xMS0wM1QxMDoxNTowMCswMDowMHw3ZjhiMWMyZS00ZDVhLTRlNmYtOWEwYi0xYzJkM2U0ZjVhNmI="}],"tags":["Webhooks"],"security":[{"PartnerApiKey":[]}],"responses":{"200":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDeliveryList"},"examples":{"example-1":{"value":{"object":"list","results":[{"id":"6e5d4c3b-2a19-4807-b6a5-948372615049","object":"webhook_delivery","endpoint_id":"1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","event_id":"2b3c4d5e-6f70-4182-93a4-b5c6d7e8f901","event_type":"consultation.approved","status":"succeeded","attempt_count":1,"next_attempt_at":"2026-11-03T12:40:00Z","last_attempt_at":"2026-11-03T12:40:00Z","delivered_at":"2026-11-03T12:40:00Z","response_status":200,"response_excerpt":"ok","error":"","created_at":"2026-11-03T12:40:00Z"}],"next_cursor":null},"summary":"Illustrative response example"}}}},"description":"Success."},"400":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"invalid_request","code":"invalid_request","message":"limit must be between 1 and 100.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"invalid_request","code":"invalid_cursor","message":"The pagination cursor is not valid.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"invalid_request","code":"brand_header_not_allowed","message":"Brand headers are not accepted on partner requests; the brand comes from the API key.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request is malformed. Partner routes also refuse the patient-app brand header with 400 brand_header_not_allowed, before authentication (no rate-limit headers)."},"401":{"headers":{"WWW-Authenticate":{"schema":{"type":"string"},"description":"The authentication scheme to use: Bearer realm=\"hi-doctor-partner\"."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"authentication_error","code":"not_authenticated","message":"Authentication credentials were not provided.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"authentication_error","code":"invalid_token","message":"The API key is not valid.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"authentication_error","code":"key_revoked","message":"The API key has been revoked.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"authentication_error","code":"key_expired","message":"The API key has expired.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"authentication_error","code":"partner_suspended","message":"The partner account is suspended.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The API key is missing, malformed, unknown, revoked or expired, or the partner account is suspended. The response carries WWW-Authenticate: Bearer realm=\"hi-doctor-partner\" and no rate-limit headers."},"403":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"permission_error","code":"insufficient_scope","message":"This endpoint requires the consultations:read, prescriptions:read, webhooks:manage scope.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"permission_error","code":"partner_not_live","message":"Live mode is not enabled for this partner.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The key lacks the consultations:read, prescriptions:read, webhooks:manage scopes (insufficient_scope), or a live key was used while live mode is not enabled for the partner (partner_not_live)."},"404":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PartnerError"},{"$ref":"#/components/schemas/PartnerFlagOffError"}]},"examples":{"example-1":{"value":{"error":{"type":"not_found","code":"not_found","message":"No such resource.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"not_found"}},"summary":"Illustrative response example"}}}},"description":"No such resource for this key's partner and mode. While the Partner API is switched off for the environment, every route answers 404 with only {\"error\": {\"type\": \"not_found\"}}, before authentication."},"429":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"rate_limited","code":"throttled","message":"Request was throttled. Expected available in 12 seconds.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"Too many requests; wait for the number of seconds in Retry-After."}},"x-pagination":{"kind":"cursor","defaultLimit":50,"maxLimit":100,"ordering":["-created_at"],"defaultOrdering":"-created_at"},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"set -eu\numask 077\n# Path values must be URL-encoded; query string excludes the leading question mark.\nurl=\"${HD_BASE_URL:?Set API origin}/v1/partner/webhook-endpoints/${HD_PATH_ENDPOINT_ID:?Set the path fixture}/deliveries/${HD_QUERY_STRING:+?${HD_QUERY_STRING}}\"\nprintf 'Accept: application/json\\n' > \"${HD_REQUEST_HEADERS_FILE:?Private header file}\"\nprintf 'Authorization: Bearer %s\\n' \"${HD_PARTNER_API_KEY:?Set partner API key}\" >> \"$HD_REQUEST_HEADERS_FILE\"\ncurl --silent --show-error --max-time 30 --request GET \\\n  --header \"@$HD_REQUEST_HEADERS_FILE\" \\\n  --dump-header \"${HD_HEADERS_FILE:?Private response header file}\" \\\n  --output \"${HD_RESPONSE_FILE:?Private response file}\" \\\n  --write-out '%{http_code}\\n%{content_type}\\n' \\\n  \"$url\"\n"},{"lang":"Python","label":"requests","source":"import os, json\nfrom pathlib import Path\nfrom urllib.parse import quote\nimport requests\nos.umask(0o077)\nurl = os.environ[\"HD_BASE_URL\"].rstrip(\"/\") + '/v1/partner/webhook-endpoints/{endpoint_id}/deliveries/'\nurl = url.replace('{endpoint_id}', quote(os.environ['HD_PATH_ENDPOINT_ID'], safe=\"\"))\nif os.environ.get(\"HD_QUERY_STRING\"): url += \"?\" + os.environ[\"HD_QUERY_STRING\"]\nheaders = {\"Accept\": 'application/json'}\nheaders[\"Authorization\"] = \"Bearer \" + os.environ[\"HD_PARTNER_API_KEY\"]\nkwargs = {}\nresponse = requests.request('GET', url, headers=headers, timeout=30, allow_redirects=False, **kwargs)\nPath(os.environ[\"HD_RESPONSE_FILE\"]).write_bytes(response.content)\nPath(os.environ[\"HD_HEADERS_FILE\"]).write_text(json.dumps(dict(response.headers)))\nprint(response.status_code)\nprint(response.headers.get(\"Content-Type\", \"\"))\n"},{"lang":"JavaScript","label":"fetch","source":"import { readFileSync, writeFileSync } from \"node:fs\";\nconst env = process.env;\nlet url = env.HD_BASE_URL.replace(/\\/$/, \"\") + \"/v1/partner/webhook-endpoints/{endpoint_id}/deliveries/\";\nurl = url.replace(\"{endpoint_id}\", encodeURIComponent(env.HD_PATH_ENDPOINT_ID));\nif (env.HD_QUERY_STRING) url += \"?\" + env.HD_QUERY_STRING;\nconst headers = { \"Accept\": \"application/json\" };\nheaders.Authorization = \"Bearer \" + env.HD_PARTNER_API_KEY;\nconst options = { method: \"GET\", headers, redirect: \"manual\", signal: AbortSignal.timeout(30000) };\nconst response = await fetch(url, options);\nwriteFileSync(env.HD_RESPONSE_FILE, Buffer.from(await response.arrayBuffer()), {mode: 0o600});\nwriteFileSync(env.HD_HEADERS_FILE, JSON.stringify(Object.fromEntries(response.headers)), {mode: 0o600});\nconsole.log(response.status);\nconsole.log(response.headers.get(\"content-type\") ?? \"\");\n"}]}},"/v1/partner/webhook-deliveries/{delivery_id}/replay/":{"post":{"operationId":"replay_webhook_delivery","description":"Resets the existing delivery for a fresh schedule: status pending, attempt_count 0, next_attempt_at now and error cleared, with the same delivery id, event id and body. It does not create another delivery. A pending delivery a worker is sending right now is left unchanged. Requires the consultations:read, prescriptions:read and webhooks:manage scopes.\n\n### Access, effects and retries\n**Ownership:** The API key's own partner and mode (test or live); another partner's or mode's records answer 404.\n\n**Prerequisites:** A partner API key with the `webhooks:manage` and `consultations:read` and `prescriptions:read` scopes.\n\n**Effects:** Resets the existing delivery row so it is sent again on a fresh retry schedule; a delivery a worker is sending at that moment is left as it is.\n\n**Retries:** Safe to repeat: each call resets the same delivery again and never creates another one; deduplicate by event id.","summary":"Replay a delivery","parameters":[{"in":"path","name":"delivery_id","schema":{"type":"string","format":"uuid"},"description":"The webhook delivery id.","required":true,"example":"9d8c7b6a-5f4e-4d3c-8b2a-1f0e9d8c7b6a"},{"in":"header","name":"Idempotency-Key","schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{8,128}$"},"description":"Accepted but not used for replay on this operation. Only POST /v1/partner/consultations stores and replays responses by Idempotency-Key.","example":"b7c1d2e3f4a5b6c7"}],"tags":["Webhooks"],"security":[{"PartnerApiKey":[]}],"responses":{"200":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDelivery"},"examples":{"example-1":{"value":{"id":"6e5d4c3b-2a19-4807-b6a5-948372615049","object":"webhook_delivery","endpoint_id":"1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","event_id":"2b3c4d5e-6f70-4182-93a4-b5c6d7e8f901","event_type":"consultation.approved","status":"pending","attempt_count":0,"next_attempt_at":"2026-11-03T12:40:00Z","last_attempt_at":null,"delivered_at":null,"response_status":null,"response_excerpt":"","error":"","created_at":"2026-11-03T12:40:00Z"},"summary":"Illustrative response example"}}}},"description":"The re-queued delivery."},"400":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"invalid_request","code":"brand_header_not_allowed","message":"Brand headers are not accepted on partner requests; the brand comes from the API key.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request is malformed. Partner routes also refuse the patient-app brand header with 400 brand_header_not_allowed, before authentication (no rate-limit headers)."},"401":{"headers":{"WWW-Authenticate":{"schema":{"type":"string"},"description":"The authentication scheme to use: Bearer realm=\"hi-doctor-partner\"."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"authentication_error","code":"not_authenticated","message":"Authentication credentials were not provided.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"authentication_error","code":"invalid_token","message":"The API key is not valid.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"authentication_error","code":"key_revoked","message":"The API key has been revoked.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"authentication_error","code":"key_expired","message":"The API key has expired.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"authentication_error","code":"partner_suspended","message":"The partner account is suspended.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The API key is missing, malformed, unknown, revoked or expired, or the partner account is suspended. The response carries WWW-Authenticate: Bearer realm=\"hi-doctor-partner\" and no rate-limit headers."},"403":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"permission_error","code":"insufficient_scope","message":"This endpoint requires the consultations:read, prescriptions:read, webhooks:manage scope.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"permission_error","code":"partner_not_live","message":"Live mode is not enabled for this partner.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The key lacks the consultations:read, prescriptions:read, webhooks:manage scopes (insufficient_scope), or a live key was used while live mode is not enabled for the partner (partner_not_live)."},"404":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PartnerError"},{"$ref":"#/components/schemas/PartnerFlagOffError"}]},"examples":{"example-1":{"value":{"error":{"type":"not_found","code":"not_found","message":"No such resource.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"not_found"}},"summary":"Illustrative response example"}}}},"description":"No such resource for this key's partner and mode. While the Partner API is switched off for the environment, every route answers 404 with only {\"error\": {\"type\": \"not_found\"}}, before authentication."},"429":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"rate_limited","code":"throttled","message":"Request was throttled. Expected available in 12 seconds.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"Too many requests; wait for the number of seconds in Retry-After."}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"set -eu\numask 077\n# Path values must be URL-encoded; query string excludes the leading question mark.\nurl=\"${HD_BASE_URL:?Set API origin}/v1/partner/webhook-deliveries/${HD_PATH_DELIVERY_ID:?Set the path fixture}/replay/${HD_QUERY_STRING:+?${HD_QUERY_STRING}}\"\nprintf 'Accept: application/json\\n' > \"${HD_REQUEST_HEADERS_FILE:?Private header file}\"\nprintf 'Authorization: Bearer %s\\n' \"${HD_PARTNER_API_KEY:?Set partner API key}\" >> \"$HD_REQUEST_HEADERS_FILE\"\ncurl --silent --show-error --max-time 30 --request POST \\\n  --header \"@$HD_REQUEST_HEADERS_FILE\" \\\n  --dump-header \"${HD_HEADERS_FILE:?Private response header file}\" \\\n  --output \"${HD_RESPONSE_FILE:?Private response file}\" \\\n  --write-out '%{http_code}\\n%{content_type}\\n' \\\n  \"$url\"\n"},{"lang":"Python","label":"requests","source":"import os, json\nfrom pathlib import Path\nfrom urllib.parse import quote\nimport requests\nos.umask(0o077)\nurl = os.environ[\"HD_BASE_URL\"].rstrip(\"/\") + '/v1/partner/webhook-deliveries/{delivery_id}/replay/'\nurl = url.replace('{delivery_id}', quote(os.environ['HD_PATH_DELIVERY_ID'], safe=\"\"))\nif os.environ.get(\"HD_QUERY_STRING\"): url += \"?\" + os.environ[\"HD_QUERY_STRING\"]\nheaders = {\"Accept\": 'application/json'}\nheaders[\"Authorization\"] = \"Bearer \" + os.environ[\"HD_PARTNER_API_KEY\"]\nkwargs = {}\nresponse = requests.request('POST', url, headers=headers, timeout=30, allow_redirects=False, **kwargs)\nPath(os.environ[\"HD_RESPONSE_FILE\"]).write_bytes(response.content)\nPath(os.environ[\"HD_HEADERS_FILE\"]).write_text(json.dumps(dict(response.headers)))\nprint(response.status_code)\nprint(response.headers.get(\"Content-Type\", \"\"))\n"},{"lang":"JavaScript","label":"fetch","source":"import { readFileSync, writeFileSync } from \"node:fs\";\nconst env = process.env;\nlet url = env.HD_BASE_URL.replace(/\\/$/, \"\") + \"/v1/partner/webhook-deliveries/{delivery_id}/replay/\";\nurl = url.replace(\"{delivery_id}\", encodeURIComponent(env.HD_PATH_DELIVERY_ID));\nif (env.HD_QUERY_STRING) url += \"?\" + env.HD_QUERY_STRING;\nconst headers = { \"Accept\": \"application/json\" };\nheaders.Authorization = \"Bearer \" + env.HD_PARTNER_API_KEY;\nconst options = { method: \"POST\", headers, redirect: \"manual\", signal: AbortSignal.timeout(30000) };\nconst response = await fetch(url, options);\nwriteFileSync(env.HD_RESPONSE_FILE, Buffer.from(await response.arrayBuffer()), {mode: 0o600});\nwriteFileSync(env.HD_HEADERS_FILE, JSON.stringify(Object.fromEntries(response.headers)), {mode: 0o600});\nconsole.log(response.status);\nconsole.log(response.headers.get(\"content-type\") ?? \"\");\n"}]}},"/v1/partner/test/consultations/{consultation_id}/advance/":{"post":{"operationId":"advance_test_consultation","description":"Test keys only. Moves a test consultation to the next status so you can exercise your integration and webhooks without a clinical review. Live keys receive 404. A consultation whose patient withdrew consent can still be advanced.\n\nThe body is validated before the transition: to must be a status, reason_code a non-empty string of at most 32 characters and question_keys a list of non-empty strings, on every request (so reason_code \"\" is refused with 422 invalid_request even when to is in_review). Only then is the transition checked (422 illegal_transition); whether reason_code is a documented reason code and question_keys are keys of the submitted questionnaire is checked only for an allowed info_requested transition. Requires the consultations:write scope.\n\n### Access, effects and retries\n**Ownership:** The API key's own partner and mode (test or live); another partner's or mode's records answer 404.\n\n**Prerequisites:** A partner API key with the `consultations:write` scope. Test keys only.\n\n**Effects:** Moves the test consultation to the next status; emits the matching webhook events.\n\n**Retries:** Each call moves the consultation again; read it first.","summary":"Move a test consultation forward","parameters":[{"in":"path","name":"consultation_id","schema":{"type":"string","format":"uuid"},"description":"The consultation id.","required":true,"example":"7f8b1c2e-4d5a-4e6f-9a0b-1c2d3e4f5a6b"},{"in":"header","name":"Idempotency-Key","schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{8,128}$"},"description":"Accepted but not used for replay on this operation. Only POST /v1/partner/consultations stores and replays responses by Idempotency-Key.","example":"b7c1d2e3f4a5b6c7"}],"tags":["Test mode"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestAdvanceRequest"},"examples":{"example-1":{"value":{"to":"in_review"},"summary":"Illustrative request example"}}}},"required":true},"security":[{"PartnerApiKey":[]}],"responses":{"200":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Consultation"},"examples":{"example-1":{"value":{"id":"7f8b1c2e-4d5a-4e6f-9a0b-1c2d3e4f5a6b","object":"consultation","mode":"test","status":"in_review","version":2,"status_history":[{"status":"submitted","version":1,"at":"2026-11-03T10:15:00Z"},{"status":"in_review","version":2,"at":"2026-11-03T12:40:00Z"}],"external_reference":"order-10045","product":{"product_id":"oral_finasteride","name":"Oral finasteride"},"country":"ES","plan_key":"one_off","dosage":"1 mg","patient":{"id":"0e1d2c3b-4a59-4687-8b9a-0b1c2d3e4f50","external_patient_id":"cust-8812"},"submitted_at":"2026-11-03T10:15:00Z","updated_at":"2026-11-03T12:40:00Z","reviewer":{"description":"Hi Doctor EU-licensed doctor","licence_country":"ES"},"decision":null,"open_info_request":null,"prescriptions":[],"billing":{"state":"pending","price":{"amount_cents":2900,"currency":"EUR"}},"cancellable":false},"summary":"Illustrative response example"}}}},"description":"The advanced test consultation."},"400":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"invalid_request","code":"parse_error","message":"JSON parse error - Expecting value: line 1 column 1 (char 0)","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"invalid_request","code":"brand_header_not_allowed","message":"Brand headers are not accepted on partner requests; the brand comes from the API key.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request is malformed. Partner routes also refuse the patient-app brand header with 400 brand_header_not_allowed, before authentication (no rate-limit headers)."},"401":{"headers":{"WWW-Authenticate":{"schema":{"type":"string"},"description":"The authentication scheme to use: Bearer realm=\"hi-doctor-partner\"."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"authentication_error","code":"not_authenticated","message":"Authentication credentials were not provided.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"authentication_error","code":"invalid_token","message":"The API key is not valid.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"authentication_error","code":"key_revoked","message":"The API key has been revoked.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"authentication_error","code":"key_expired","message":"The API key has expired.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"authentication_error","code":"partner_suspended","message":"The partner account is suspended.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The API key is missing, malformed, unknown, revoked or expired, or the partner account is suspended. The response carries WWW-Authenticate: Bearer realm=\"hi-doctor-partner\" and no rate-limit headers."},"403":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"permission_error","code":"insufficient_scope","message":"This endpoint requires the consultations:write scope.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"permission_error","code":"partner_not_live","message":"Live mode is not enabled for this partner.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The key lacks the consultations:write scope (insufficient_scope), or a live key was used while live mode is not enabled for the partner (partner_not_live)."},"404":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PartnerError"},{"$ref":"#/components/schemas/PartnerFlagOffError"}]},"examples":{"example-1":{"value":{"error":{"type":"not_found","code":"not_found","message":"No such resource.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"not_found"}},"summary":"Illustrative response example"}}}},"description":"No such resource for this key's partner and mode. While the Partner API is switched off for the environment, every route answers 404 with only {\"error\": {\"type\": \"not_found\"}}, before authentication."},"422":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"validation_error","code":"illegal_transition","message":"A test consultation in status submitted cannot move to approved.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-2":{"value":{"error":{"type":"validation_error","code":"invalid_request","message":"The request body is invalid.","errors":[{"field":"to","code":"invalid_choice","message":"This is not a valid choice."}],"request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-3":{"value":{"error":{"type":"validation_error","code":"invalid_request","message":"The request body is invalid.","errors":[{"field":"reason_code","code":"blank","message":"This field may not be blank."}],"request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-4":{"value":{"error":{"type":"validation_error","code":"invalid_request","message":"reason_code is not a documented reason code.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"},"example-5":{"value":{"error":{"type":"validation_error","code":"invalid_request","message":"question_keys must be keys of the submitted questionnaire.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"The request body is invalid."},"429":{"headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window for the most restrictive limit that applies. Absent on refusals made before the throttles run (brand header, flag off, partner_not_live)."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix time, in seconds, when the window frees up."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerError"},"examples":{"example-1":{"value":{"error":{"type":"rate_limited","code":"throttled","message":"Request was throttled. Expected available in 12 seconds.","request_id":"req_5f0c3a9d2b7e4f18a6c1d9e3b2a7f4c8"}},"summary":"Illustrative response example"}}}},"description":"Too many requests; wait for the number of seconds in Retry-After."}},"x-codeSamples":[{"lang":"Shell","label":"curl","source":"set -eu\numask 077\n# Path values must be URL-encoded; query string excludes the leading question mark.\nurl=\"${HD_BASE_URL:?Set API origin}/v1/partner/test/consultations/${HD_PATH_CONSULTATION_ID:?Set the path fixture}/advance/${HD_QUERY_STRING:+?${HD_QUERY_STRING}}\"\nprintf 'Accept: application/json\\n' > \"${HD_REQUEST_HEADERS_FILE:?Private header file}\"\nprintf 'Authorization: Bearer %s\\n' \"${HD_PARTNER_API_KEY:?Set partner API key}\" >> \"$HD_REQUEST_HEADERS_FILE\"\ncurl --silent --show-error --max-time 30 --request POST \\\n  --header \"@$HD_REQUEST_HEADERS_FILE\" \\\n  --dump-header \"${HD_HEADERS_FILE:?Private response header file}\" \\\n  --output \"${HD_RESPONSE_FILE:?Private response file}\" \\\n  --write-out '%{http_code}\\n%{content_type}\\n' \\\n  --header 'Content-Type: application/json' \\\n  --data-binary \"@${HD_REQUEST_FILE:?JSON fixture file}\" \\\n  \"$url\"\n"},{"lang":"Python","label":"requests","source":"import os, json\nfrom pathlib import Path\nfrom urllib.parse import quote\nimport requests\nos.umask(0o077)\nurl = os.environ[\"HD_BASE_URL\"].rstrip(\"/\") + '/v1/partner/test/consultations/{consultation_id}/advance/'\nurl = url.replace('{consultation_id}', quote(os.environ['HD_PATH_CONSULTATION_ID'], safe=\"\"))\nif os.environ.get(\"HD_QUERY_STRING\"): url += \"?\" + os.environ[\"HD_QUERY_STRING\"]\nheaders = {\"Accept\": 'application/json'}\nheaders[\"Authorization\"] = \"Bearer \" + os.environ[\"HD_PARTNER_API_KEY\"]\nkwargs = {}\nkwargs[\"json\"] = json.loads(Path(os.environ[\"HD_REQUEST_FILE\"]).read_text())\nresponse = requests.request('POST', url, headers=headers, timeout=30, allow_redirects=False, **kwargs)\nPath(os.environ[\"HD_RESPONSE_FILE\"]).write_bytes(response.content)\nPath(os.environ[\"HD_HEADERS_FILE\"]).write_text(json.dumps(dict(response.headers)))\nprint(response.status_code)\nprint(response.headers.get(\"Content-Type\", \"\"))\n"},{"lang":"JavaScript","label":"fetch","source":"import { readFileSync, writeFileSync } from \"node:fs\";\nconst env = process.env;\nlet url = env.HD_BASE_URL.replace(/\\/$/, \"\") + \"/v1/partner/test/consultations/{consultation_id}/advance/\";\nurl = url.replace(\"{consultation_id}\", encodeURIComponent(env.HD_PATH_CONSULTATION_ID));\nif (env.HD_QUERY_STRING) url += \"?\" + env.HD_QUERY_STRING;\nconst headers = { \"Accept\": \"application/json\" };\nheaders.Authorization = \"Bearer \" + env.HD_PARTNER_API_KEY;\nconst options = { method: \"POST\", headers, redirect: \"manual\", signal: AbortSignal.timeout(30000) };\nheaders[\"Content-Type\"] = \"application/json\";\noptions.body = readFileSync(env.HD_REQUEST_FILE, \"utf8\");\nconst response = await fetch(url, options);\nwriteFileSync(env.HD_RESPONSE_FILE, Buffer.from(await response.arrayBuffer()), {mode: 0o600});\nwriteFileSync(env.HD_HEADERS_FILE, JSON.stringify(Object.fromEntries(response.headers)), {mode: 0o600});\nconsole.log(response.status);\nconsole.log(response.headers.get(\"content-type\") ?? \"\");\n"}]}}},"components":{"schemas":{"AddressInput":{"type":"object","properties":{"line1":{"type":"string","description":"line1 and line2 are stored as one street line, joined with \", \"; together (separator included) they must not exceed 200 characters, or the request is refused with 422 invalid_request (field patient.address). Nothing is truncated.","maxLength":200},"line2":{"type":"string","description":"line1 and line2 are stored as one street line, joined with \", \"; together (separator included) they must not exceed 200 characters, or the request is refused with 422 invalid_request (field patient.address). Nothing is truncated.","maxLength":200},"city":{"type":"string","maxLength":120,"description":"City."},"postal_code":{"type":"string","maxLength":20,"description":"Postal code."},"country":{"type":"string","description":"Country of residence, ISO 3166-1 alpha-2 or alpha-3. It must be one of the 27 EU member states (AT, BE, BG, CY, CZ, DE, DK, EE, ES, FI, FR, GR, HR, HU, IE, IT, LT, LU, LV, MT, NL, PL, PT, RO, SE, SI, SK). Defaults to the request country when absent or blank. A well-formed code outside the EU is refused with 422 invalid_request and an error item with field patient.address.country and code residence_outside_eu. A malformed value (null, or longer than 3 characters) fails field validation first: 422 invalid_request with the same field and the field validation code (null or max_length).","maxLength":3}}},"AnswerFormatEnum":{"enum":["entries-v1"],"type":"string"},"Billing":{"type":"object","properties":{"state":{"oneOf":[{"$ref":"#/components/schemas/BillingStateEnum"},{"$ref":"#/components/schemas/NullEnum"}],"description":"pending, billable, void or reversed."},"price":{"allOf":[{"$ref":"#/components/schemas/Money"}],"description":"Price agreed for the consultation."}},"required":["price","state"]},"BillingStateEnum":{"enum":["pending","billable","void","reversed"],"type":"string"},"ConsentInput":{"type":"object","properties":{"terms_accepted":{"type":"boolean","enum":[true],"description":"The patient accepted Hi Doctor's terms for the consultation. Must be true."},"privacy_accepted":{"type":"boolean","enum":[true],"description":"The patient was shown Hi Doctor's privacy notice. Must be true."},"clinical_data_processing_accepted":{"type":"boolean","enum":[true],"description":"The patient agreed that their health data is processed for the consultation. Must be true."},"accepted_at":{"type":"string","format":"date-time","description":"When the patient gave consent (ISO 8601)."},"notice_version":{"type":"string","maxLength":32,"minLength":1,"pattern":"\\S","description":"The version of the patient notice you showed; must be the current one."},"evidence_reference":{"type":"string","maxLength":128,"description":"Your reference to the stored consent record."},"ip":{"type":"string","nullable":true,"anyOf":[{"format":"ipv4"},{"format":"ipv6"}],"description":"The IPv4 or IPv6 address the patient accepted from, or null; any other value is refused with 422 invalid_request."}},"required":["accepted_at","clinical_data_processing_accepted","notice_version","privacy_accepted","terms_accepted"]},"Consultation":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"The consultation id."},"object":{"allOf":[{"$ref":"#/components/schemas/ConsultationObjectEnum"}],"description":"Always consultation."},"mode":{"allOf":[{"$ref":"#/components/schemas/ModeEnum"}],"description":"test or live, from the API key."},"status":{"allOf":[{"$ref":"#/components/schemas/ConsultationStatusEnum"}],"description":"Current consultation status."},"version":{"type":"integer","description":"Increases with every change to the consultation's public state, i.e. every field of this object except version, status_history, updated_at and product.name (catalogue copy: a product rename does not advance it). Events carry the same number."},"status_history":{"type":"array","items":{"$ref":"#/components/schemas/StatusHistoryEntry"},"description":"Every status change, oldest first."},"external_reference":{"type":"string","nullable":true,"description":"Your order or case reference; null when none was sent."},"product":{"allOf":[{"$ref":"#/components/schemas/ConsultationProduct"}],"description":"The product."},"country":{"type":"string","description":"ISO 3166-1 alpha-2 country code."},"plan_key":{"type":"string","nullable":true,"description":"The chosen plan."},"dosage":{"type":"string","nullable":true,"description":"The chosen dosage; null when the product has none."},"patient":{"allOf":[{"$ref":"#/components/schemas/ConsultationPatientRef"}],"description":"The patient."},"submitted_at":{"type":"string","format":"date-time","description":"When the consultation was submitted."},"updated_at":{"type":"string","format":"date-time","description":"When it last changed."},"reviewer":{"description":"Present once the reviewing doctor has opened the case.","anyOf":[{"$ref":"#/components/schemas/Reviewer"},{"type":"string","nullable":true,"enum":[null]}]},"decision":{"description":"The clinical decision; null until one is recorded.","anyOf":[{"$ref":"#/components/schemas/Decision"},{"type":"string","nullable":true,"enum":[null]}]},"open_info_request":{"description":"The open information request; null when there is none.","anyOf":[{"$ref":"#/components/schemas/OpenInfoRequest"},{"type":"string","nullable":true,"enum":[null]}]},"prescriptions":{"type":"array","items":{"$ref":"#/components/schemas/Prescription"},"description":"Prescriptions issued for the consultation."},"billing":{"allOf":[{"$ref":"#/components/schemas/Billing"}],"description":"Billing state and price of the consultation."},"cancellable":{"type":"boolean","description":"true exactly when POST /consultations/{consultation_id}/cancel would succeed now: the consultation is submitted, has no prescription, and (live) no doctor has opened it, no decision or electronic prescription signing has started, and no prescription issuance or replacement is in flight."}},"required":["billing","cancellable","country","decision","dosage","external_reference","id","mode","object","open_info_request","patient","plan_key","prescriptions","product","reviewer","status","status_history","submitted_at","updated_at","version"]},"ConsultationAnswerEntry":{"type":"object","description":"One entries-v1 answer: question_key plus exactly one payload, value, values or skip, as the question kind\nrequires.","properties":{"question_key":{"type":"string","description":"A question key from the questionnaire schema. Required, a non-empty string: an entry without one is refused with 422 invalid_answers.","minLength":1,"pattern":"\\S"},"value":{"description":"The answer for option, confirm, upload and scalar kinds: an option value, a list of option values for multi kinds, true for a confirm, or an upload_id for an upload question."},"values":{"type":"object","additionalProperties":{},"description":"The answer for input, iief5 and likert-grid kinds: an object of field values, for example {\"height\": 172, \"weight\": 96}."},"skip":{"allOf":[{"$ref":"#/components/schemas/SkipEnum"}],"description":"true to skip an optional question (required false). A skipped entry carries only its locator (question_key, or on an information-request answer its alias key), skip and, optionally, the question's own step_id: value, values, option_inputs or any other member alongside it is refused with 422 invalid_answers."},"option_inputs":{"type":"object","additionalProperties":{"oneOf":[{"type":"string"},{"type":"number"},{"type":"boolean"}]},"description":"Extra input for options that require it, keyed by option value."}},"required":["question_key"],"anyOf":[{"not":{"required":["skip"]}},{"properties":{"question_key":{"description":"The question key of the skipped question."},"step_id":{"description":"Optional: the skipped question's own step_id."},"skip":{"description":"true: the question is skipped."}},"additionalProperties":false}]},"ConsultationCreateRequest":{"type":"object","properties":{"product_id":{"type":"string","maxLength":64,"minLength":1,"pattern":"\\S","description":"Product id from GET /v1/partner/products."},"country":{"type":"string","maxLength":3,"minLength":1,"pattern":"\\S","description":"ISO 3166-1 alpha-2 code of a country your partner catalogue offers the product in."},"plan_key":{"type":"string","maxLength":32,"minLength":1,"pattern":"\\S","description":"One of the product's plans[].plan_key for the country."},"dosage":{"type":"string","default":"","maxLength":32,"description":"One of the product's dosages[].dosage, when the product lists dosages."},"locale":{"type":"string","default":"","maxLength":2,"description":"Two-letter language code saved as the language of a new patient account; empty uses your partner default."},"schema_version":{"type":"string","maxLength":64,"minLength":1,"pattern":"\\S","description":"schema_version of the questionnaire you rendered."},"schema_hash":{"type":"string","maxLength":64,"minLength":1,"pattern":"\\S","description":"schema_hash of the questionnaire you rendered, with or without a locale."},"external_reference":{"type":"string","default":"","maxLength":128,"description":"Your order or case reference, unique per partner and mode."},"patient":{"allOf":[{"$ref":"#/components/schemas/PatientInput"}],"description":"Patient identity, consent and optional national identifier."},"answers":{"type":"array","items":{"$ref":"#/components/schemas/ConsultationAnswerEntry"},"minItems":1,"description":"The patient's answers as entries-v1 entries keyed by question_key. Answer only the steps that apply after branching."}},"required":["answers","country","patient","plan_key","product_id","schema_hash","schema_version"]},"ConsultationList":{"type":"object","properties":{"object":{"allOf":[{"$ref":"#/components/schemas/ListObjectEnum"}],"description":"The object type."},"results":{"type":"array","items":{"$ref":"#/components/schemas/Consultation"},"description":"The items on this page."},"next_cursor":{"type":"string","nullable":true,"description":"Cursor for the next page; null on the last page."}},"required":["next_cursor","object","results"]},"ConsultationObjectEnum":{"enum":["consultation"],"type":"string"},"ConsultationPatientRef":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"The patient id."},"external_patient_id":{"type":"string","description":"Your stable identifier for the patient."}},"required":["external_patient_id","id"]},"ConsultationProduct":{"type":"object","properties":{"product_id":{"type":"string","description":"Product id from GET /v1/partner/products."},"name":{"type":"string","description":"Product display name."}},"required":["name","product_id"]},"ConsultationStatusEnum":{"enum":["submitted","in_review","info_requested","approved","rejected","cancelled"],"type":"string"},"Decision":{"type":"object","properties":{"outcome":{"allOf":[{"$ref":"#/components/schemas/DecisionOutcomeEnum"}],"description":"approved or rejected."},"reason_category":{"oneOf":[{"$ref":"#/components/schemas/ReasonCategoryEnum"},{"$ref":"#/components/schemas/NullEnum"}],"description":"Category of a rejection; null otherwise."},"decided_at":{"type":"string","format":"date-time","nullable":true,"description":"When the decision was recorded."}},"required":["decided_at","outcome","reason_category"]},"DecisionOutcomeEnum":{"enum":["approved","rejected"],"type":"string"},"EventsEnum":{"type":"string","enum":["*","consultation.approved","consultation.cancelled","consultation.in_review","consultation.info_requested","consultation.rejected","consultation.submitted","consultation.updated","prescription.cancelled","prescription.issued","prescription.replaced"]},"FileUploadRequest":{"type":"object","properties":{"file":{"type":"string","format":"binary","description":"JPEG, PNG or PDF, at most 10 MB. The type is detected from the bytes."},"purpose":{"allOf":[{"$ref":"#/components/schemas/UploadPurposeEnum"}],"default":"photo","description":"photo or document."},"sha256":{"type":"string","description":"Optional hex digest; a mismatch is refused."}},"required":["file"]},"Fulfilment":{"type":"object","properties":{"route":{"allOf":[{"$ref":"#/components/schemas/FulfilmentRouteEnum"}],"description":"How the patient obtains the medication; see the fulfilment guide."}},"required":["route"]},"FulfilmentRouteEnum":{"enum":["national_e_prescription","signed_pdf","pharmacy_delivery","cross_border_pdf"],"type":"string"},"IdentityDocumentInput":{"type":"object","description":"An identity document, validated with the same rules as a patient's own profile.","properties":{"type":{"allOf":[{"$ref":"#/components/schemas/IdentityDocumentInputTypeEnum"}],"description":"passport or id_card."},"number":{"type":"string","description":"The document number as printed; it is normalised.","maxLength":50,"minLength":1,"pattern":"\\S"},"issuing_state":{"type":"string","description":"ISO 3166-1 alpha-2 or alpha-3 code of the country that issued the document.","maxLength":3,"minLength":1,"pattern":"\\S"}},"required":["issuing_state","number","type"]},"IdentityDocumentInputTypeEnum":{"enum":["passport","id_card"],"type":"string"},"InfoRequest":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"The information request id."},"object":{"allOf":[{"$ref":"#/components/schemas/InfoRequestObjectEnum"}],"description":"Always info_request."},"consultation_id":{"type":"string","format":"uuid","description":"The consultation this belongs to."},"status":{"allOf":[{"$ref":"#/components/schemas/InfoRequestStatusEnum"}],"description":"open, answered or withdrawn."},"reason_code":{"allOf":[{"$ref":"#/components/schemas/ReasonCodeEnum"}],"description":"Why the doctor needs more information."},"reason_label":{"type":"string","description":"Readable form of reason_code."},"question_keys":{"type":"array","items":{"type":"string"},"description":"Question keys the doctor asked to be answered again."},"questions":{"type":"array","items":{"$ref":"#/components/schemas/InfoRequestQuestion"},"description":"The questions to answer."},"documents_requested":{"type":"array","items":{"type":"string"},"description":"Kinds of documents the doctor asked for; answer them with upload ids."},"requested_at":{"type":"string","format":"date-time","nullable":true,"description":"When the doctor raised the request."},"answered_at":{"type":"string","format":"date-time","nullable":true,"description":"When the request was answered; null while it is open."},"withdrawn_at":{"type":"string","format":"date-time","nullable":true,"description":"When the request was withdrawn; null if it was not."},"answered_question_keys":{"type":"array","items":{"type":"string"},"description":"The question keys the answer covered, in answer order; empty until answered. The answer values are never returned."},"answer_count":{"type":"integer","minimum":0,"description":"How many answer entries Hi Doctor accepted; 0 until answered."},"answer_document_count":{"type":"integer","minimum":0,"description":"How many files the answer attached; 0 until answered."}},"required":["answer_count","answer_document_count","answered_at","answered_question_keys","consultation_id","documents_requested","id","object","question_keys","questions","reason_code","reason_label","requested_at","status","withdrawn_at"]},"InfoRequestAnswerEntry":{"type":"object","description":"One entries-v1 answer: question_key plus exactly one payload, value, values or skip, as the question kind\nrequires.","properties":{"question_key":{"type":"string","nullable":true,"description":"A key named in the information request's question_keys; any other key, or none, is refused with 422 unexpected_question_key. Send question_key, or leave it out, empty or null and send its alias key."},"value":{"description":"The answer for option, confirm, upload and scalar kinds: an option value, a list of option values for multi kinds, true for a confirm, or an upload_id for an upload question."},"values":{"type":"object","additionalProperties":{},"description":"The answer for input, iief5 and likert-grid kinds: an object of field values, for example {\"height\": 172, \"weight\": 96}."},"skip":{"allOf":[{"$ref":"#/components/schemas/SkipEnum"}],"description":"true to skip an optional question (required false). A skipped entry carries only its locator (question_key, or on an information-request answer its alias key), skip and, optionally, the question's own step_id: value, values, option_inputs or any other member alongside it is refused with 422 invalid_answers."},"option_inputs":{"type":"object","additionalProperties":{"oneOf":[{"type":"string"},{"type":"number"},{"type":"boolean"}]},"description":"Extra input for options that require it, keyed by option value."},"key":{"type":"string","description":"Alias of question_key, accepted and normalised to question_key; used only when question_key is absent, empty or null, and then it must be a non-empty string. When question_key is set, key is ignored."}},"allOf":[{"anyOf":[{"not":{"required":["skip"]}},{"properties":{"question_key":{"description":"The question key of the skipped question."},"key":{"description":"Alias of question_key, when question_key is left out, empty or null."},"step_id":{"description":"Optional: the skipped question's own step_id."},"skip":{"description":"true: the question is skipped."}},"additionalProperties":false}]},{"anyOf":[{"required":["question_key"],"properties":{"question_key":{"type":"string","minLength":1,"pattern":"\\S","description":"A non-empty question key; when set, key is ignored."}}},{"required":["key"],"properties":{"key":{"type":"string","minLength":1,"pattern":"\\S","description":"A non-empty alias of question_key, used when question_key is left out, empty or null."},"question_key":{"type":"string","nullable":true,"enum":["",null],"description":"Left out, empty or null when the entry is located by key."}}}]}]},"InfoRequestAnswerRequest":{"type":"object","description":"Send at least one answer, one upload or (on an identity_document_required request) the identity document;\na body with none of them is refused with 422 invalid_request before the answer's contents are checked. null is\nthe same as omitting a member; any other type is refused with 422 invalid_request.","properties":{"answers":{"type":"array","items":{"$ref":"#/components/schemas/InfoRequestAnswerEntry"},"nullable":true,"description":"Answers for the requested question keys only; may be omitted when uploads or the identity document are sent. Each question may be answered once (otherwise 422 invalid_answers)."},"uploads":{"type":"array","items":{"type":"string","format":"uuid"},"maxItems":10,"uniqueItems":true,"nullable":true,"description":"upload_id values for requested documents: at most 10 (more is refused with 422 invalid_request). Each upload can be used once: a duplicate, here or between uploads and an upload answer in answers, is refused with 422 upload_unusable, as is an upload that does not exist, expired or was already used."},"identity_document":{"description":"The patient's identity document. Accepted only on a request with reason_code identity_document_required (on any other request it is refused with 422 invalid_request, field identity_document, code unexpected_field), and required there: once the body carries an answer or an upload, a missing document is refused with 422 identity_document_required (a body with no answer, upload or document is refused with 422 invalid_request first). Validated like patient.identity_document on submission: a document that breaks the rules (unknown issuing state, wrong check letter, number format), a non-object or unknown members is refused with 422 invalid_request, field identity_document, code invalid_identity_document; a structural error keeps its nested field error instead, for example field identity_document.type with code invalid_choice for an unsupported type, or identity_document.number with code required, blank or max_length. A document different from the one stored for the patient is refused with 409 patient_conflict. Never echoed or returned; live mode stores it on the patient's account, test mode stores nothing.","anyOf":[{"$ref":"#/components/schemas/IdentityDocumentAnswer"},{"type":"string","nullable":true,"enum":[null]}]}},"anyOf":[{"required":["answers"],"properties":{"answers":{"type":"array","minItems":1,"description":"At least one answer."}}},{"required":["uploads"],"properties":{"uploads":{"type":"array","minItems":1,"description":"At least one upload_id."}}},{"required":["identity_document"],"properties":{"identity_document":{"type":"object","description":"The identity document, on an identity_document_required request."}}}]},"InfoRequestList":{"type":"object","properties":{"object":{"allOf":[{"$ref":"#/components/schemas/ListObjectEnum"}],"description":"The object type."},"results":{"type":"array","items":{"$ref":"#/components/schemas/InfoRequest"},"description":"The items on this page."}},"required":["object","results"]},"InfoRequestObjectEnum":{"enum":["info_request"],"type":"string"},"InfoRequestQuestion":{"type":"object","properties":{"key":{"type":"string","description":"The question_key to answer."},"label":{"type":"string","nullable":true,"description":"The question's text in the request's locale (the locale query parameter, else the partner's default locale; English for an unsupported locale and per missing message). Display text, never a message key: null when no catalogue has text for it, and key still identifies the question."},"type":{"type":"string","description":"Kind of answer expected."}},"required":["key","label","type"]},"InfoRequestStatusEnum":{"enum":["open","answered","withdrawn"],"type":"string"},"ListObjectEnum":{"enum":["list"],"type":"string"},"Medication":{"type":"object","properties":{"name":{"type":"string","description":"Medication name."},"prescribed_label":{"type":"string","description":"The medication as prescribed."},"dosage":{"type":"string","nullable":true,"description":"Prescribed dosage."},"supply_days":{"type":"integer","nullable":true,"description":"Days of supply prescribed."}},"required":["dosage","name","prescribed_label","supply_days"]},"ModeEnum":{"enum":["live","test"],"type":"string"},"Money":{"type":"object","properties":{"amount_cents":{"type":"integer","description":"Amount in the currency's minor unit."},"currency":{"type":"string","description":"ISO 4217 currency code."}},"required":["amount_cents","currency"]},"NationalIdentifierInput":{"type":"object","description":"A national identifier, validated against the product country's identifier rules.","properties":{"type":{"type":"string","description":"An identifier type of the product country (not the residence country). Types by country: BE: niss, eid_card_number; ES: dni, nie; IE: ppsn; IT: codice_fiscale, stp, eni; PT: nif; SE: personnummer.","maxLength":32,"minLength":1,"pattern":"\\S"},"value":{"type":"string","description":"The identifier as printed; it is validated (format and check digits) and normalised.","maxLength":32,"minLength":1,"pattern":"\\S"}},"required":["type","value"]},"NullEnum":{"enum":[null],"type":"string","nullable":true},"OpenInfoRequest":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"The information request id."},"reason_code":{"allOf":[{"$ref":"#/components/schemas/ReasonCodeEnum"}],"description":"Why the doctor needs more information."},"requested_at":{"type":"string","format":"date-time","nullable":true,"description":"When the doctor raised the request."}},"required":["id","reason_code","requested_at"]},"PartnerError":{"type":"object","properties":{"error":{"allOf":[{"$ref":"#/components/schemas/PartnerErrorDetail"}],"description":"The error."}},"required":["error"]},"PartnerErrorDetail":{"type":"object","properties":{"type":{"allOf":[{"$ref":"#/components/schemas/PartnerErrorDetailTypeEnum"}],"description":"Error type, for example validation_error or rate_limited; see Errors in the partner guides."},"code":{"type":"string","description":"A stable, machine-readable error code."},"message":{"type":"string","description":"A human-readable explanation; do not parse it."},"errors":{"type":"array","items":{"$ref":"#/components/schemas/PartnerErrorItem"},"description":"Field-level problems, when any."},"request_id":{"type":"string","description":"Quote this identifier when you contact support."},"current":{"allOf":[{"$ref":"#/components/schemas/SchemaVersion"}],"description":"On 409 schema_outdated: the schema_version and schema_hash currently published."},"current_notice_version":{"type":"string","description":"On 422 consent_notice_outdated: the patient notice version to show and record."}},"required":["code","message","request_id","type"]},"PartnerErrorDetailTypeEnum":{"enum":["api_error","authentication_error","conflict","invalid_request","not_found","permission_error","rate_limited","validation_error"],"type":"string"},"PartnerErrorItem":{"type":"object","properties":{"field":{"type":"string","nullable":true,"description":"The request field the problem refers to, or null when none."},"question_key":{"type":"string","nullable":true,"description":"The questionnaire question the problem refers to, or null when the entry named none."},"code":{"type":"string","description":"Machine-readable error code for this item."},"message":{"type":"string","description":"Human-readable message. It never repeats the submitted value."},"min":{"type":"number","format":"double","description":"On out_of_range: the lowest allowed value, when the schema sets one."},"max":{"type":"number","format":"double","description":"On out_of_range: the highest allowed value, when the schema sets one."}},"required":["code","message"]},"PartnerPatient":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"The patient id."},"object":{"allOf":[{"$ref":"#/components/schemas/PatientObjectEnum"}],"description":"Always patient."},"mode":{"allOf":[{"$ref":"#/components/schemas/ModeEnum"}],"description":"test or live."},"external_patient_id":{"type":"string","description":"Your stable identifier for the patient."},"created_at":{"type":"string","format":"date-time","description":"When the patient was first submitted."},"consent":{"allOf":[{"$ref":"#/components/schemas/PatientConsent"}],"description":"The patient's consent record."},"consultation_count":{"type":"integer","description":"Consultations submitted for this patient in this mode."}},"required":["consent","consultation_count","created_at","external_patient_id","id","mode","object"]},"PartnerPatientList":{"type":"object","properties":{"object":{"allOf":[{"$ref":"#/components/schemas/ListObjectEnum"}],"description":"The object type."},"results":{"type":"array","items":{"$ref":"#/components/schemas/PartnerPatient"},"description":"The items on this page."},"next_cursor":{"type":"string","nullable":true,"description":"Cursor for the next page; null on the last page."}},"required":["next_cursor","object","results"]},"PatchedWebhookEndpointUpdateRequest":{"type":"object","properties":{"url":{"type":"string","maxLength":2048,"minLength":1,"pattern":"\\S","description":"The https URL (port 443) that receives deliveries."},"events":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/EventsEnum"},"anyOf":[{"items":{"enum":["*"]}},{"items":{"enum":["consultation.approved","consultation.cancelled","consultation.in_review","consultation.info_requested","consultation.rejected","consultation.submitted","consultation.updated","prescription.cancelled","prescription.issued","prescription.replaced"]}}],"description":"[\"*\"] for every event, or a non-empty list of event types; \"*\" cannot be combined with event types and duplicates are ignored. A value that is not a list, a blank entry or an entry longer than 48 characters is refused first with 422 invalid_request; a list of strings that is not [\"*\"] or a non-empty list of documented event types is then refused with 422 invalid_webhook_events."},"description":{"type":"string","maxLength":200,"description":"Your description of the endpoint."},"status":{"allOf":[{"$ref":"#/components/schemas/WebhookEndpointSettableStatusEnum"}],"description":"enabled or disabled. Re-enabling resets the failure streak."}}},"PatientConsent":{"type":"object","properties":{"notice_version":{"type":"string","nullable":true,"description":"Version of the patient notice the patient accepted."},"accepted_at":{"type":"string","format":"date-time","nullable":true,"description":"When the patient gave consent."},"withdrawn_at":{"type":"string","format":"date-time","nullable":true,"description":"When consent was withdrawn; null if it was not."}},"required":["accepted_at","notice_version","withdrawn_at"]},"PatientInput":{"type":"object","properties":{"external_patient_id":{"type":"string","maxLength":128,"minLength":1,"pattern":"\\S","description":"Your stable identifier for the patient."},"email":{"type":"string","format":"email","minLength":1,"pattern":"\\S","description":"The patient's own email address."},"first_name":{"type":"string","maxLength":150,"minLength":1,"pattern":"\\S","description":"Legal first name."},"last_name":{"type":"string","maxLength":150,"minLength":1,"pattern":"\\S","description":"Legal last name."},"date_of_birth":{"type":"string","format":"date","description":"Date of birth (YYYY-MM-DD)."},"sex":{"allOf":[{"$ref":"#/components/schemas/SexEnum"}],"description":"female, male, other or unspecified."},"phone":{"type":"string","maxLength":16,"description":"Phone number in E.164 format."},"address":{"allOf":[{"$ref":"#/components/schemas/AddressInput"}],"description":"The patient's postal address."},"national_identifier":{"allOf":[{"$ref":"#/components/schemas/NationalIdentifierInput"}],"description":"Optional. The patient's national identifier for the product country, needed where that country's prescribing requires one (without it, a submission is refused with 422 patient_identifier_required unless the identifier is already stored for the patient). A type the product country does not use, or a value that fails validation, is refused with 422 invalid_request and an error item with field patient.national_identifier and code invalid_identifier; a missing, blank or too long type or value keeps its own field error instead (for example field patient.national_identifier.value, code required or max_length); the submitted value is never echoed in errors or returned in any response or event. Live mode stores it on the patient's account the first time; a later submission for the same patient with a different identifier is refused with 409 patient_conflict and never overwrites the stored one. Test mode validates it and stores nothing."},"identity_document":{"allOf":[{"$ref":"#/components/schemas/IdentityDocumentInput"}],"description":"Optional. The patient's identity document: a national id_card or a passport, with the country that issued it. Electronic prescribing in some countries needs it before a doctor can approve. It is validated like a patient's own profile: every number must be 5 to 20 letters or digits after spaces, dots and hyphens are removed, and for a few issuing countries an id_card number is also checked against its national format and check character. A document that breaks these rules (unknown issuing state, wrong check character, number format) is refused with 422 invalid_request and an error item with field patient.identity_document and code invalid_identity_document. A structural error keeps its nested field error instead: a value that is not an object is field patient.identity_document.non_field_errors with code invalid, an unsupported type is field patient.identity_document.type with code invalid_choice, and a missing, blank or too long member is that member's field (for example patient.identity_document.number) with code required, blank or max_length. It is never echoed in errors or returned in any response or event. Live mode stores it on the patient's account the first time; a later submission for the same patient with a different document is refused with 409 patient_conflict and never overwrites the stored one. Test mode validates it and stores nothing. If the reviewing doctor needs it later, they ask with the information request reason identity_document_required."},"consent":{"allOf":[{"$ref":"#/components/schemas/ConsentInput"}],"description":"The patient's consent, captured by you before submission."}},"required":["consent","date_of_birth","email","external_patient_id","first_name","last_name","sex"]},"PatientObjectEnum":{"enum":["patient"],"type":"string"},"Ping":{"type":"object","properties":{"ok":{"type":"boolean","description":"True when the key is accepted."},"partner":{"type":"string","description":"Your partner slug."},"mode":{"allOf":[{"$ref":"#/components/schemas/ModeEnum"}],"description":"test or live, from the API key."},"key_id":{"type":"string","description":"The public identifier of the API key that made the request."}},"required":["key_id","mode","ok","partner"]},"Prescription":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"The prescription id."},"consultation_id":{"type":"string","format":"uuid","nullable":true,"description":"The consultation this belongs to."},"status":{"allOf":[{"$ref":"#/components/schemas/PrescriptionStatusEnum"}],"description":"issued until the prescription is cancelled. It does not say whether the prescription can still be used: read active, and compare expires_at yourself."},"active":{"type":"boolean","description":"false once the prescription is cancelled or no longer published to the patient, for example after a dispensing is recorded during a manual recovery. It does not check expiry: an expired prescription can stay active true, so compare expires_at yourself."},"issued_at":{"type":"string","format":"date-time","nullable":true,"description":"When the prescription was issued."},"cancelled_at":{"type":"string","format":"date-time","nullable":true,"description":"When the prescription was cancelled; null if it was not."},"expires_at":{"type":"string","format":"date-time","nullable":true,"description":"When the prescription expires, or null when not recorded. Always null in test mode."},"dispensing_country":{"type":"string","nullable":true,"description":"ISO 3166-1 alpha-2 country where the prescription is dispensed, or null when not recorded. Always null in test mode."},"medication":{"allOf":[{"$ref":"#/components/schemas/Medication"}],"description":"The prescribed medication."},"fulfilment":{"allOf":[{"$ref":"#/components/schemas/Fulfilment"}],"description":"How the patient obtains the medication."},"collected_at":{"type":"string","format":"date-time","nullable":true,"description":"When the medication was collected, when known; otherwise null."},"replaced_by":{"type":"string","format":"uuid","nullable":true,"description":"The prescription that replaced this one."}},"required":["active","cancelled_at","collected_at","consultation_id","dispensing_country","expires_at","fulfilment","id","issued_at","medication","replaced_by","status"]},"PrescriptionList":{"type":"object","properties":{"object":{"allOf":[{"$ref":"#/components/schemas/ListObjectEnum"}],"description":"The object type."},"results":{"type":"array","items":{"$ref":"#/components/schemas/Prescription"},"description":"The items on this page."},"next_cursor":{"type":"string","nullable":true,"description":"Cursor for the next page; null on the last page."}},"required":["next_cursor","object","results"]},"PrescriptionStatusEnum":{"enum":["issued","cancelled"],"type":"string"},"Product":{"type":"object","properties":{"product_id":{"type":"string","description":"Product id from GET /v1/partner/products."},"name":{"type":"string","description":"Product display name."},"category":{"type":"string","description":"Product category."},"questionnaire_slug":{"type":"string","description":"Slug of the questionnaire to fetch for this product."},"schema_version":{"type":"string","nullable":true,"description":"Current questionnaire version."},"schema_hash":{"type":"string","nullable":true,"description":"Current questionnaire hash (without locale)."},"country":{"type":"string","description":"ISO 3166-1 alpha-2 country code."},"plans":{"type":"array","items":{"$ref":"#/components/schemas/ProductPlan"},"description":"Plans available for the product in this country."},"dosages":{"type":"array","items":{"$ref":"#/components/schemas/ProductDosage"},"description":"Dosages available for the product in this country."},"price":{"allOf":[{"$ref":"#/components/schemas/Money"}],"description":"Price agreed for this product and country."}},"required":["category","country","dosages","name","plans","price","product_id","questionnaire_slug","schema_hash","schema_version"]},"ProductDosage":{"type":"object","properties":{"dosage":{"type":"string","description":"Dosage value to submit."},"label":{"type":"string","description":"Readable dosage."}},"required":["dosage","label"]},"ProductList":{"type":"object","properties":{"object":{"allOf":[{"$ref":"#/components/schemas/ListObjectEnum"}],"description":"The object type."},"results":{"type":"array","items":{"$ref":"#/components/schemas/Product"},"description":"The items on this page."}},"required":["object","results"]},"ProductPlan":{"type":"object","properties":{"plan_key":{"type":"string","description":"Plan key to submit."},"supply_days":{"type":"integer","nullable":true,"description":"Days of supply in the plan."}},"required":["plan_key","supply_days"]},"Questionnaire":{"type":"object","properties":{"slug":{"type":"string","description":"Questionnaire slug."},"schema_version":{"type":"string","description":"Version of the questionnaire schema."},"schema_hash":{"type":"string","description":"The hash of exactly the schema in this response (localized when locale is set). Send it back with the submission: the current version's hash is accepted with or without a locale and in any supported locale; a stale hash is refused with 409 schema_outdated."},"locale":{"type":"string","nullable":true,"description":"The locale the labels are written in: the requested one, or en when it is not supported. null when no locale was requested (labels carry message keys only)."},"answer_format":{"allOf":[{"$ref":"#/components/schemas/AnswerFormatEnum"}],"description":"The answer format to submit: entries-v1."},"upload_answer":{"allOf":[{"$ref":"#/components/schemas/UploadAnswer"}],"description":"How to answer an upload question."},"schema":{"type":"object","additionalProperties":{},"description":"The questionnaire: steps, questions, options and validation rules."}},"required":["answer_format","locale","schema","schema_hash","schema_version","slug","upload_answer"]},"ReasonCategoryEnum":{"enum":["safety","not_eligible"],"type":"string"},"ReasonCodeEnum":{"enum":["answer_unclear","answer_inconsistent","photo_quality","photo_missing","measurement_check","medication_history","medical_history","identity_check","document_missing","identity_document_required","other_clarification"],"type":"string"},"Reviewer":{"type":"object","properties":{"description":{"type":"string","description":"Who reviewed the consultation."},"licence_country":{"type":"string","nullable":true,"description":"ISO 3166-1 alpha-2 country of the reviewing doctor's licence; null when not recorded."}},"required":["description","licence_country"]},"SchemaVersion":{"type":"object","properties":{"schema_version":{"type":"string","description":"The published schema_version."},"schema_hash":{"type":"string","description":"A hash accepted for the published version."}},"required":["schema_hash","schema_version"]},"SexEnum":{"enum":["female","male","other","unspecified"],"type":"string"},"SkipEnum":{"enum":[true],"type":"boolean"},"StatusHistoryEntry":{"type":"object","properties":{"status":{"allOf":[{"$ref":"#/components/schemas/ConsultationStatusEnum"}],"description":"The status entered."},"version":{"type":"integer","description":"Consultation version at this status."},"at":{"type":"string","format":"date-time","description":"When the consultation entered this status."}},"required":["at","status","version"]},"TestAdvanceRequest":{"type":"object","description":"Every member is validated for shape before the transition is checked; reason_code membership and\nquestion_keys against the questionnaire are checked only for an allowed info_requested transition.","properties":{"to":{"allOf":[{"$ref":"#/components/schemas/ToEnum"}],"description":"The status to move the test consultation to."},"reason_code":{"type":"string","default":"other_clarification","description":"An information request reason code. Its shape (a non-empty string of at most 32 characters) is validated on every request, before the transition, so \"\" is refused with 422 invalid_request even when to is not info_requested; whether it is a documented reason code is checked only for an allowed info_requested transition.","maxLength":32,"minLength":1,"pattern":"\\S"},"question_keys":{"type":"array","items":{"type":"string","maxLength":128,"minLength":1,"pattern":"\\S"},"description":"Question keys for an info_requested transition. Their shape is validated on every request; whether they are keys of the submitted questionnaire is checked only for an allowed info_requested transition."}},"required":["to"]},"ToEnum":{"enum":["in_review","info_requested","approved","rejected","prescription_issued"],"type":"string"},"Upload":{"type":"object","properties":{"upload_id":{"type":"string","format":"uuid","description":"The upload id; use it as the answer to an upload question."},"object":{"allOf":[{"$ref":"#/components/schemas/UploadObjectEnum"}],"description":"Always upload."},"purpose":{"allOf":[{"$ref":"#/components/schemas/UploadPurposeEnum"}],"description":"photo or document."},"content_type":{"type":"string","description":"Detected content type of the file."},"bytes":{"type":"integer","description":"File size in bytes."},"sha256":{"type":"string","description":"SHA-256 hex digest of the file."},"width":{"type":"integer","nullable":true,"description":"Image width in pixels; null for documents."},"height":{"type":"integer","nullable":true,"description":"Image height in pixels; null for documents."},"status":{"allOf":[{"$ref":"#/components/schemas/UploadStatusEnum"}],"description":"pending until a consultation or information request answer uses it (attached). An upload never used before expires_at is expired: it no longer counts towards the storage quota, cannot be used, and is deleted with its file by a periodic cleanup."},"expires_at":{"type":"string","format":"date-time","nullable":true,"description":"When the upload expires if it is still unused."}},"required":["bytes","content_type","expires_at","height","object","purpose","sha256","status","upload_id","width"]},"UploadAnswer":{"type":"object","properties":{"value":{"type":"string","description":"Answer an upload question with the upload_id returned by POST /files."}},"required":["value"]},"UploadObjectEnum":{"enum":["upload"],"type":"string"},"UploadPurposeEnum":{"enum":["photo","document"],"type":"string"},"UploadStatusEnum":{"enum":["pending","attached","expired"],"type":"string"},"WebhookDelivery":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"The delivery id."},"object":{"allOf":[{"$ref":"#/components/schemas/WebhookDeliveryObjectEnum"}],"description":"Always webhook_delivery."},"endpoint_id":{"type":"string","format":"uuid","description":"The webhook endpoint the delivery is for."},"event_id":{"type":"string","format":"uuid","description":"The event id. A replay keeps the same id; use it to deduplicate."},"event_type":{"type":"string","description":"The event type, for example consultation.approved."},"status":{"allOf":[{"$ref":"#/components/schemas/WebhookDeliveryStatusEnum"}],"description":"pending, succeeded, failed or exhausted."},"attempt_count":{"type":"integer","description":"Delivery attempts made so far."},"next_attempt_at":{"type":"string","format":"date-time","description":"When the next attempt is due. Never null: it keeps the last scheduled time after a success or once retries are exhausted."},"last_attempt_at":{"type":"string","format":"date-time","nullable":true,"description":"When the last attempt was made; null before the first attempt."},"delivered_at":{"type":"string","format":"date-time","nullable":true,"description":"When the endpoint accepted the delivery; null until then."},"response_status":{"type":"integer","nullable":true,"description":"HTTP status your endpoint returned on the last attempt; null if none was received."},"response_excerpt":{"type":"string","description":"The start of your endpoint's last response body; an empty string when none."},"error":{"type":"string","description":"Why the last attempt failed; an empty string when it did not fail."},"created_at":{"type":"string","format":"date-time","description":"When the delivery was queued."}},"required":["attempt_count","created_at","delivered_at","endpoint_id","error","event_id","event_type","id","last_attempt_at","next_attempt_at","object","response_excerpt","response_status","status"]},"WebhookDeliveryList":{"type":"object","properties":{"object":{"allOf":[{"$ref":"#/components/schemas/ListObjectEnum"}],"description":"The object type."},"results":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDelivery"},"description":"The items on this page."},"next_cursor":{"type":"string","nullable":true,"description":"Cursor for the next page; null on the last page."}},"required":["next_cursor","object","results"]},"WebhookDeliveryObjectEnum":{"enum":["webhook_delivery"],"type":"string"},"WebhookDeliveryStatusEnum":{"enum":["pending","succeeded","failed","exhausted"],"type":"string"},"WebhookEndpoint":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"The webhook endpoint id."},"object":{"allOf":[{"$ref":"#/components/schemas/WebhookEndpointObjectEnum"}],"description":"Always webhook_endpoint."},"url":{"type":"string","format":"uri","description":"The https URL (port 443) that receives deliveries."},"description":{"type":"string","description":"Your description of the endpoint."},"mode":{"allOf":[{"$ref":"#/components/schemas/ModeEnum"}],"description":"test or live, from the API key that created it."},"events":{"type":"array","items":{"type":"string"},"description":"Event types sent to the endpoint, or [\"*\"] for all of them."},"status":{"allOf":[{"$ref":"#/components/schemas/WebhookEndpointStatusEnum"}],"description":"enabled, disabled or auto_disabled."},"failure_streak":{"type":"integer","description":"Consecutive failed deliveries; reset by a successful delivery or by re-enabling the endpoint."},"last_success_at":{"type":"string","format":"date-time","nullable":true,"description":"When a delivery to the endpoint last succeeded."},"last_failure_at":{"type":"string","format":"date-time","nullable":true,"description":"When a delivery to the endpoint last failed."},"previous_secret_expires_at":{"type":"string","format":"date-time","nullable":true,"description":"After a rotation, when the previous secret stops signing deliveries; otherwise null."},"created_at":{"type":"string","format":"date-time","description":"When it was created."},"updated_at":{"type":"string","format":"date-time","description":"When it last changed."}},"required":["created_at","description","events","failure_streak","id","last_failure_at","last_success_at","mode","object","previous_secret_expires_at","status","updated_at","url"]},"WebhookEndpointCreateRequest":{"type":"object","properties":{"url":{"type":"string","maxLength":2048,"minLength":1,"pattern":"\\S","description":"The https URL (port 443) that receives deliveries."},"events":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/EventsEnum"},"anyOf":[{"items":{"enum":["*"]}},{"items":{"enum":["consultation.approved","consultation.cancelled","consultation.in_review","consultation.info_requested","consultation.rejected","consultation.submitted","consultation.updated","prescription.cancelled","prescription.issued","prescription.replaced"]}}],"description":"[\"*\"] for every event, or a non-empty list of event types; \"*\" cannot be combined with event types and duplicates are ignored. A value that is not a list, a blank entry or an entry longer than 48 characters is refused first with 422 invalid_request; a list of strings that is not [\"*\"] or a non-empty list of documented event types is then refused with 422 invalid_webhook_events."},"description":{"type":"string","default":"","maxLength":200,"description":"Your description of the endpoint."},"mode":{"allOf":[{"$ref":"#/components/schemas/ModeEnum"}],"description":"test or live. Defaults to the API key mode; another mode is refused with 422 mode_mismatch."}},"required":["events","url"]},"WebhookEndpointList":{"type":"object","properties":{"object":{"allOf":[{"$ref":"#/components/schemas/ListObjectEnum"}],"description":"The object type."},"results":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEndpoint"},"description":"The items on this page."}},"required":["object","results"]},"WebhookEndpointObjectEnum":{"enum":["webhook_endpoint"],"type":"string"},"WebhookEndpointSettableStatusEnum":{"enum":["enabled","disabled"],"type":"string"},"WebhookEndpointStatusEnum":{"enum":["enabled","disabled","auto_disabled"],"type":"string"},"WebhookEndpointWithSecret":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"The webhook endpoint id."},"object":{"allOf":[{"$ref":"#/components/schemas/WebhookEndpointObjectEnum"}],"description":"Always webhook_endpoint."},"url":{"type":"string","format":"uri","description":"The https URL (port 443) that receives deliveries."},"description":{"type":"string","description":"Your description of the endpoint."},"mode":{"allOf":[{"$ref":"#/components/schemas/ModeEnum"}],"description":"test or live, from the API key that created it."},"events":{"type":"array","items":{"type":"string"},"description":"Event types sent to the endpoint, or [\"*\"] for all of them."},"status":{"allOf":[{"$ref":"#/components/schemas/WebhookEndpointStatusEnum"}],"description":"enabled, disabled or auto_disabled."},"failure_streak":{"type":"integer","description":"Consecutive failed deliveries; reset by a successful delivery or by re-enabling the endpoint."},"last_success_at":{"type":"string","format":"date-time","nullable":true,"description":"When a delivery to the endpoint last succeeded."},"last_failure_at":{"type":"string","format":"date-time","nullable":true,"description":"When a delivery to the endpoint last failed."},"previous_secret_expires_at":{"type":"string","format":"date-time","nullable":true,"description":"After a rotation, when the previous secret stops signing deliveries; otherwise null."},"created_at":{"type":"string","format":"date-time","description":"When it was created."},"updated_at":{"type":"string","format":"date-time","description":"When it last changed."},"secret":{"type":"string","description":"The signing secret: whsec_ followed by the standard base64 encoding of 32 random bytes, which are the HMAC key. It is shown only in this response; store it now."}},"required":["created_at","description","events","failure_streak","id","last_failure_at","last_success_at","mode","object","previous_secret_expires_at","secret","status","updated_at","url"]},"PartnerFlagOffError":{"type":"object","description":"The reduced body every route answers while the Partner API is switched off for the environment: no code, message or request_id.","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["not_found"],"description":"Always not_found."}},"required":["type"],"additionalProperties":false,"description":"Only the error type; no code, message or request_id."}},"required":["error"],"additionalProperties":false},"IdentityDocumentAnswer":{"type":"object","description":"An identity document in an information-request answer: only type, number and issuing_state; any other member is refused with 422 invalid_request, field identity_document, code invalid_identity_document.","properties":{"type":{"allOf":[{"$ref":"#/components/schemas/IdentityDocumentInputTypeEnum"}],"description":"passport or id_card."},"number":{"type":"string","description":"The document number as printed; it is normalised.","maxLength":50,"minLength":1,"pattern":"\\S"},"issuing_state":{"type":"string","description":"ISO 3166-1 alpha-2 or alpha-3 code of the country that issued the document.","maxLength":3,"minLength":1,"pattern":"\\S"}},"required":["issuing_state","number","type"],"additionalProperties":false}},"securitySchemes":{"PartnerApiKey":{"type":"http","scheme":"bearer","bearerFormat":"API key","description":"Partner API key sent as Authorization: Bearer <api key>. Test keys and live keys are separate."}}},"servers":[{"url":"https://api.hi-doctor.ai","description":"Production"}],"tags":[{"name":"Connectivity","description":"Check that a key works."},{"name":"Products","description":"What you can offer, where, and at what price."},{"name":"Questionnaires","description":"The questions a patient answers for a product."},{"name":"Files","description":"Photos and documents attached to answers."},{"name":"Consultations","description":"Submit consultations and follow their review."},{"name":"Patients","description":"The patients you submitted consultations for, and their consent."},{"name":"Information requests","description":"Questions the doctor sends back during review."},{"name":"Prescriptions","description":"Prescriptions issued after an approval."},{"name":"Webhooks","description":"Receive events instead of polling."},{"name":"Test mode","description":"Tools that only work with test keys."}]}
