set -eu
umask 077
# Path values must be URL-encoded; query string excludes the leading question mark.
url="${HD_BASE_URL:?Set API origin}/v1/questionnaire/consultations/${HD_PATH_ID:?Set the path fixture}/images/${HD_QUERY_STRING:+?${HD_QUERY_STRING}}"
printf 'X-Brand-Slug: hi-doctor\nAccept: application/json\n' > "${HD_REQUEST_HEADERS_FILE:?Private header file}"
printf 'Authorization: Bearer %s\n' "${HD_ACCESS_TOKEN:?Set backend JWT}" >> "$HD_REQUEST_HEADERS_FILE"
form_args=()
while IFS= read -r -d "" form_value; do
form_args+=(--form-string "$form_value")
done < <(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}")
curl --silent --show-error --max-time 30 --request POST \
--header "@$HD_REQUEST_HEADERS_FILE" \
--dump-header "${HD_HEADERS_FILE:?Private response header file}" \
--output "${HD_RESPONSE_FILE:?Private response file}" \
--write-out '%{http_code}\n%{content_type}\n' \
--form 'image=@'"${HD_UPLOAD_FILE:?Synthetic upload};type=${HD_UPLOAD_CONTENT_TYPE:?Upload MIME}" \
${form_args[@]+"${form_args[@]}"} \
"$url"import os, json
from pathlib import Path
from urllib.parse import quote
import requests
os.umask(0o077)
url = os.environ["HD_BASE_URL"].rstrip("/") + '/v1/questionnaire/consultations/{id}/images/'
url = url.replace('{id}', quote(os.environ['HD_PATH_ID'], safe=""))
if os.environ.get("HD_QUERY_STRING"): url += "?" + os.environ["HD_QUERY_STRING"]
headers = {"X-Brand-Slug": "hi-doctor", "Accept": 'application/json'}
headers["Authorization"] = "Bearer " + os.environ["HD_ACCESS_TOKEN"]
kwargs = {}
kwargs["files"] = {'image': ("synthetic-upload", Path(os.environ["HD_UPLOAD_FILE"]).read_bytes(), os.environ["HD_UPLOAD_CONTENT_TYPE"])}
kwargs["data"] = json.loads(Path(os.environ["HD_REQUEST_FILE"]).read_text())
response = requests.request('POST', url, headers=headers, timeout=30, allow_redirects=False, **kwargs)
Path(os.environ["HD_RESPONSE_FILE"]).write_bytes(response.content)
Path(os.environ["HD_HEADERS_FILE"]).write_text(json.dumps(dict(response.headers)))
print(response.status_code)
print(response.headers.get("Content-Type", ""))
import { readFileSync, writeFileSync } from "node:fs";
const env = process.env;
let url = env.HD_BASE_URL.replace(/\/$/, "") + "/v1/questionnaire/consultations/{id}/images/";
url = url.replace("{id}", encodeURIComponent(env.HD_PATH_ID));
if (env.HD_QUERY_STRING) url += "?" + env.HD_QUERY_STRING;
const headers = { "X-Brand-Slug": "hi-doctor", "Accept": "application/json" };
headers.Authorization = "Bearer " + env.HD_ACCESS_TOKEN;
const options = { method: "POST", headers, redirect: "manual", signal: AbortSignal.timeout(30000) };
options.body = new FormData();
options.body.append("image", new Blob([readFileSync(env.HD_UPLOAD_FILE)], {type: env.HD_UPLOAD_CONTENT_TYPE}), "synthetic-upload");
for (const [key, value] of Object.entries(JSON.parse(readFileSync(env.HD_REQUEST_FILE, "utf8")))) { if (value != null) options.body.append(key, String(value)); }
const response = await fetch(url, options);
writeFileSync(env.HD_RESPONSE_FILE, Buffer.from(await response.arrayBuffer()), {mode: 0o600});
writeFileSync(env.HD_HEADERS_FILE, JSON.stringify(Object.fromEntries(response.headers)), {mode: 0o600});
console.log(response.status);
console.log(response.headers.get("content-type") ?? "");
<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.hi-doctor.ai/v1/questionnaire/consultations/{id}/images/",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"image\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\nsynthetic-upload.png\r\n-----011000010111000001101001--",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: multipart/form-data; boundary=---011000010111000001101001"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.hi-doctor.ai/v1/questionnaire/consultations/{id}/images/"
payload := strings.NewReader("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"image\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\nsynthetic-upload.png\r\n-----011000010111000001101001--")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "multipart/form-data; boundary=---011000010111000001101001")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.hi-doctor.ai/v1/questionnaire/consultations/{id}/images/")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "multipart/form-data; boundary=---011000010111000001101001")
.body("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"image\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\nsynthetic-upload.png\r\n-----011000010111000001101001--")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.hi-doctor.ai/v1/questionnaire/consultations/{id}/images/")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'multipart/form-data; boundary=---011000010111000001101001'
request.body = "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"image\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\nsynthetic-upload.png\r\n-----011000010111000001101001--"
response = http.request(request)
puts response.read_body{
"id": "057fb9e0-9f09-5dff-a98f-6ce1926b0d78",
"url": "https://api.hi-doctor.ai/v1/questionnaire/consultations/b0e81246-c46e-5ac2-9675-06bf07784bf4/images/057fb9e0-9f09-5dff-a98f-6ce1926b0d78/content/",
"filename": "synthetic.png",
"uploaded_at": "2026-10-05T19:54:56.393075Z",
"consultation_id": "b0e81246-c46e-5ac2-9675-06bf07784bf4",
"available": true
}Create a consultation photo
Attach one photograph to a consultation you own.
multipart/form-data, and the field is named image — sending file is refused explicitly. The upload must decode as PNG, JPEG or WebP, weigh at most 10 MB, and measure at most 10000 px on a side and 25 megapixels in total; the server decodes it to check rather than trusting the declared content type.
A consultation holds at most 10 images; the eleventh is refused with 400. This route is for patient accounts; any other account gets an empty 403.
Access, effects and retries
Ownership: Patient owner only.
Prerequisites: Authentication and permission to access the consultation are required.
Effects: Creates image and storage object on consultation.
Retries: Retry may create another image; no idempotency key exposed.
set -eu
umask 077
# Path values must be URL-encoded; query string excludes the leading question mark.
url="${HD_BASE_URL:?Set API origin}/v1/questionnaire/consultations/${HD_PATH_ID:?Set the path fixture}/images/${HD_QUERY_STRING:+?${HD_QUERY_STRING}}"
printf 'X-Brand-Slug: hi-doctor\nAccept: application/json\n' > "${HD_REQUEST_HEADERS_FILE:?Private header file}"
printf 'Authorization: Bearer %s\n' "${HD_ACCESS_TOKEN:?Set backend JWT}" >> "$HD_REQUEST_HEADERS_FILE"
form_args=()
while IFS= read -r -d "" form_value; do
form_args+=(--form-string "$form_value")
done < <(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}")
curl --silent --show-error --max-time 30 --request POST \
--header "@$HD_REQUEST_HEADERS_FILE" \
--dump-header "${HD_HEADERS_FILE:?Private response header file}" \
--output "${HD_RESPONSE_FILE:?Private response file}" \
--write-out '%{http_code}\n%{content_type}\n' \
--form 'image=@'"${HD_UPLOAD_FILE:?Synthetic upload};type=${HD_UPLOAD_CONTENT_TYPE:?Upload MIME}" \
${form_args[@]+"${form_args[@]}"} \
"$url"import os, json
from pathlib import Path
from urllib.parse import quote
import requests
os.umask(0o077)
url = os.environ["HD_BASE_URL"].rstrip("/") + '/v1/questionnaire/consultations/{id}/images/'
url = url.replace('{id}', quote(os.environ['HD_PATH_ID'], safe=""))
if os.environ.get("HD_QUERY_STRING"): url += "?" + os.environ["HD_QUERY_STRING"]
headers = {"X-Brand-Slug": "hi-doctor", "Accept": 'application/json'}
headers["Authorization"] = "Bearer " + os.environ["HD_ACCESS_TOKEN"]
kwargs = {}
kwargs["files"] = {'image': ("synthetic-upload", Path(os.environ["HD_UPLOAD_FILE"]).read_bytes(), os.environ["HD_UPLOAD_CONTENT_TYPE"])}
kwargs["data"] = json.loads(Path(os.environ["HD_REQUEST_FILE"]).read_text())
response = requests.request('POST', url, headers=headers, timeout=30, allow_redirects=False, **kwargs)
Path(os.environ["HD_RESPONSE_FILE"]).write_bytes(response.content)
Path(os.environ["HD_HEADERS_FILE"]).write_text(json.dumps(dict(response.headers)))
print(response.status_code)
print(response.headers.get("Content-Type", ""))
import { readFileSync, writeFileSync } from "node:fs";
const env = process.env;
let url = env.HD_BASE_URL.replace(/\/$/, "") + "/v1/questionnaire/consultations/{id}/images/";
url = url.replace("{id}", encodeURIComponent(env.HD_PATH_ID));
if (env.HD_QUERY_STRING) url += "?" + env.HD_QUERY_STRING;
const headers = { "X-Brand-Slug": "hi-doctor", "Accept": "application/json" };
headers.Authorization = "Bearer " + env.HD_ACCESS_TOKEN;
const options = { method: "POST", headers, redirect: "manual", signal: AbortSignal.timeout(30000) };
options.body = new FormData();
options.body.append("image", new Blob([readFileSync(env.HD_UPLOAD_FILE)], {type: env.HD_UPLOAD_CONTENT_TYPE}), "synthetic-upload");
for (const [key, value] of Object.entries(JSON.parse(readFileSync(env.HD_REQUEST_FILE, "utf8")))) { if (value != null) options.body.append(key, String(value)); }
const response = await fetch(url, options);
writeFileSync(env.HD_RESPONSE_FILE, Buffer.from(await response.arrayBuffer()), {mode: 0o600});
writeFileSync(env.HD_HEADERS_FILE, JSON.stringify(Object.fromEntries(response.headers)), {mode: 0o600});
console.log(response.status);
console.log(response.headers.get("content-type") ?? "");
<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.hi-doctor.ai/v1/questionnaire/consultations/{id}/images/",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"image\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\nsynthetic-upload.png\r\n-----011000010111000001101001--",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: multipart/form-data; boundary=---011000010111000001101001"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.hi-doctor.ai/v1/questionnaire/consultations/{id}/images/"
payload := strings.NewReader("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"image\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\nsynthetic-upload.png\r\n-----011000010111000001101001--")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "multipart/form-data; boundary=---011000010111000001101001")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.hi-doctor.ai/v1/questionnaire/consultations/{id}/images/")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "multipart/form-data; boundary=---011000010111000001101001")
.body("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"image\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\nsynthetic-upload.png\r\n-----011000010111000001101001--")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.hi-doctor.ai/v1/questionnaire/consultations/{id}/images/")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'multipart/form-data; boundary=---011000010111000001101001'
request.body = "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"image\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\nsynthetic-upload.png\r\n-----011000010111000001101001--"
response = http.request(request)
puts response.read_body{
"id": "057fb9e0-9f09-5dff-a98f-6ce1926b0d78",
"url": "https://api.hi-doctor.ai/v1/questionnaire/consultations/b0e81246-c46e-5ac2-9675-06bf07784bf4/images/057fb9e0-9f09-5dff-a98f-6ce1926b0d78/content/",
"filename": "synthetic.png",
"uploaded_at": "2026-10-05T19:54:56.393075Z",
"consultation_id": "b0e81246-c46e-5ac2-9675-06bf07784bf4",
"available": true
}Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Headers
Send X-Brand-Slug: hi-doctor. Public auth/questionnaire/attribution routes resolve the brand header; authenticated scope also uses the live user brand. Do not claim the header is universally server-required: default-brand fallback exists.
hi-doctor Optional key that makes a retry safe. Generate one value per logical operation (8 to 128 ASCII letters, digits, underscores or hyphens) and resend the same value on every retry. A repeat of a completed request returns its original status with Idempotent-Replayed: true and a short body of state, resource_ids and response_hash; read the resources to recover their content. Reusing a key with a different request returns 422 idempotency_key_reuse; a request still running returns 409 idempotency_in_progress. Keys are kept for 30 days. See https://docs.hi-doctor.ai/api-reference/idempotency.
^[A-Za-z0-9_-]{8,128}$Path Parameters
Identifier returned by the corresponding authorized collection; use only a record visible to the signed-in patient.
Body
Image bytes uploaded for the owned consultation.
Response
Authorized consultation image metadata.
Image identifier.
Authorized image content URL.
Display filename.
Upload timestamp.
Owning consultation identifier.
Whether this image remains available.
Present for an image originally supplied through the questionnaire.
Question key for a questionnaire image; null if unavailable.