Skip to content

Latest commit

 

History

History
733 lines (566 loc) · 46.2 KB

File metadata and controls

733 lines (566 loc) · 46.2 KB

Bulutklinik SDK — Canonical Design (SSOT)

This file is the single source of truth (SSOT) for every official Bulutklinik SDK. All language packages (JavaScript/TypeScript, PHP, Python, Go, Java, C#, C++) are hand-written but MUST implement exactly the contract described here. The canonical copy lives at dev-kits/DESIGN.md; an identical copy is vendored into each language repository and re-synced whenever this file changes.

Wire contract is derived from dev-kits/Bulutklinik.postman_collection.json ("Bulutklinik API — Randevu & Ödeme Akışı"), validated against the BulutklinikAPI source (Laravel 8.12, OAuth2/Passport).

  • Spec version: 0.6.0 (completes the registration flows and closes flow-gaps found in the API audit: adds auth.confirmRegistrationEmail (the e-mail-branch middle step that register actually needs), the social sign-up pair auth.verifyRegistrationSocial/auth.registerSocial, the password-reset pair auth.forgotPassword/auth.resetPassword, appointments.list/appointments.reservations (the source of the event_id that cancel needs), and the new addresses group required by laboratory.order; 0.5.0 added auth.verifyRegistration; 0.4.0 added laboratory + diets; 0.3.0 added skin + meals; 0.2.0 added the §7.2 escape hatch)
  • API: BulutklinikAPI v3
  • Scope: 11 services / 48 endpoints (patient persona). Designed to grow.

1. Scope

The SDKs cover the patient appointment-and-payment flow, health measurements, and AI image analysis:

Service Endpoints Purpose
auth 11 Login, 2FA, refresh, registration (verify/e-mail-confirm/create), social sign-up, password reset, logout
doctors 5 Branches, locations, quick/filtered search, detail
slots 1 Doctor availability (materialized slots)
appointments 5 Reserve, physical appointment, cancel, list, reservations
payments 5 Discount check, saved cards, pay (3DS)
measures 8 Health measurements (CRUD, list, graph, partner)
skin 1 AI skin-lesion analysis ("Cildimde Neyim Var")
meals 1 AI meal-photo calorie/nutrition estimation
laboratory 5 Lab results, orderable test catalog, test pre-order
diets 2 Diet lists (list + detail) written by the dietitian
addresses 4 The patient's saved addresses (needed by laboratory.order)

Out of scope for this collection (may be added later): "Anlık randevu" (programs), video-call (calls). The SDK surface is designed so new services slot in as new resource groups without breaking existing ones.


2. Environments & transport

2.1 Base URLs

Env Base URL
production https://api.bulutklinik.com/api/v3
test https://apitest.bulutklinik.com/api/v3
local https://api-bulutklinik.test/api/v3 (Herd)

The client accepts either a named environment preset or an explicit base URL. Default: production.

2.2 Required headers

Header Value Notes
Accept application/json Always.
Content-Type application/json On requests with a body.
lang tr (default), en, de, az Configurable per-client and per-request.
Authorization Bearer <accessToken> Protected endpoints only. Omitted on public endpoints; partner endpoint uses the partner token.

2.3 HTTP methods

Endpoints use GET, POST, PUT, DELETE as specified per endpoint in §6. Path parameters (e.g. {id}, {type}, {page}) are URL segments, not query string. Request bodies are JSON.


3. Response envelope

Every API response is a JSON envelope:

{
  "resultType": 0,            // integer state code (see §3.1)
  "errorType": "validation",  // optional; string label OR numeric code (see note)
  "errorMessage": "",        // optional, human-readable (localized via `lang`)
  "successMessage": "",      // optional
  "data": { /* payload */ }   // endpoint-specific; may be null, object, array, or string
}

errorType is polymorphic (verified live): some endpoints return a string label (e.g. "validation"), others return a numeric code (e.g. 1). SDKs must accept both — only treat errorType as a refinement hint when it is a string; never assume it is a string (e.g. don't call string methods on it unguarded).

A call is successful when the HTTP status is 2xx and resultType == 0. SDKs unwrap and return data to the caller on success; otherwise they raise a typed error (§4).

3.1 resultType state machine

Value Name SDK behavior
0 Success Return data.
1 Error Raise ApiError (or a more specific subtype based on HTTP status / errorType).
2 Logout Clear the token store, raise AuthenticationError (session revoked).
3 Update Raise ApiError with an "update required" marker (client/app too old).
4 Refresh Token expired. Not returned by refreshApi; returned by the global handler on any protected call that receives an expired/invalid token (HTTP 401). Triggers the auto-refresh+retry flow (§5.4).

Implementation note: resultType 4 is the canonical refresh signal, but a bare HTTP 401 (without a parseable envelope) MUST be treated identically.


4. Error model

All SDKs expose one error hierarchy with a common base. Names follow each language's convention (e.g. BulutklinikError / ApiError in TS, exceptions in PHP/Python, error values implementing an interface in Go, exception classes in Java/C#/C++).

BulutklinikError                  (base — all SDK errors derive from this)
├── TransportError                (network failure, timeout, DNS, TLS — no HTTP response)
└── ApiError                      (got an HTTP response that wasn't a success)
    ├── ValidationError           (422, or errorType=validation)
    ├── AuthenticationError       (401 / resultType 2 logout / failed refresh)
    ├── AuthorizationError        (403 — authenticated but not permitted/scoped)
    ├── NotFoundError             (404)
    └── RateLimitError            (429 — throttled; carries Retry-After if present)

Each ApiError carries: httpStatus, resultType, errorType, errorMessage, the raw data, and the originating request (method + path) for debugging. Mapping precedence: logout (resultType == 2) → string errorType == "validation" → HTTP status (401→Auth, 403→Authz, 404→NotFound, 422→Validation, 429→RateLimit) → otherwise (incl. numeric errorType, or success HTTP with resultType != 0) → ApiError. Because errorType may be numeric (§3), guard before string-matching it.


5. Authentication & token lifecycle

OAuth2 via Laravel Passport. Access token lifetime ~30 days, refresh token ~130 days. The token grant happens server-side inside connectApi (no direct oauth/token HTTP call from the SDK).

5.1 Login — auth.connect

POST /general/connectApi (also aliased at root /connectApi). Public (no Bearer).

Request body:

Field Required Notes
apiUserName Identifier per loginMode (email / TC / phone / user_id).
apiUserPassword ✓* Required except social / afterRegister modes.
apiClientId OAuth client id.
apiSecretKey OAuth client secret.
loginMode email | identity | phone | user_id | social | afterRegister.
withPhoneNumber Some installs require it in phone mode.

loginMode social / afterRegister skip password validation (validateForPassportPasswordGrant).

Success → data: { access_token, refresh_token, password_policy }. The SDK persists both tokens via the token store (§5.5).

2FA branch: if SMS 2FA is enabled (sms_2fa_status=1), data.access_token is absent and data.response carries an encrypted blob. The SDK surfaces this as a two-factor challenge (typed result, not an error) so the caller can collect the SMS code and call auth.connectWithTwoFactor.

5.2 2FA verification — auth.connectWithTwoFactor

POST /general/connectApiWithTwoFactor. Public (middleware verifies the SMS code inside the encrypted blob).

Request body:

Field Required Notes
smsVerificationCode The code the user received by SMS.
response The encrypted blob from connect's data.response.

(The collection also sends tokenInfo, but the server ignores it — the real token is decrypted from response. SDKs send only smsVerificationCode + response.)

Success → data: { access_token, refresh_token }. Token is not re-minted here; it was minted during connect and is returned now.

5.3 Token refresh — auth.refresh

POST /general/refreshApi. Public. Uses the Passport refresh_token grant.

Request body: { refreshToken, clientId, clientSecretKey }. Success → data: { access_token, refresh_token } (both rotated; persist both).

5.4 Silent auto-refresh + retry (mandatory in every SDK)

On any protected call:

  1. Send the request with the current access token.
  2. If the response is 401 or resultType == 4, and a refresh token exists, and this request has not already been retried: a. Call auth.refresh with the stored refresh token + client credentials. b. Persist the new tokens. c. Retry the original request once.
  3. If the refresh call itself fails, or resultType == 2 (logout), clear the token store and raise AuthenticationError.
  4. Auto-refresh must be concurrency-safe: simultaneous 401s share a single in-flight refresh (no refresh stampede). Single-threaded SDKs (e.g. plain JS) gate on one shared promise; threaded SDKs (Java/C#/Go/C++) use a mutex.

The retry is bounded to one attempt to prevent loops.

5.5 Token store (pluggable)

A TokenStore abstraction holds the access + refresh tokens. Default implementation is in-memory. Consumers may inject a custom store (file, DB, secure storage). Required operations (named per language):

  • get access token / get refresh token
  • set tokens (access, refresh) — atomically
  • clear (on logout / revoked session)

5.6 Registration — verifyRegistration → (confirmRegistrationEmail) → register

Registration is a multi-call flow. verifyRegistration returns confirmationType:

  • "sms" → feed its response + the SMS code straight into register.
  • "email" → the user gets the code by e-mail; call confirmRegistrationEmail first (it verifies the e-mail code, sends an SMS code, and returns a fresh response blob), then feed that blob + the SMS code into register.

⚠️ A headerless SDK caller always gets "email". The SMS branch of verifyAddingNewPatient only triggers when an appversion header ≤ 5.27 is present; SDKs send no such header, so confirmRegistrationEmail is a required middle step, not optional. register (guarded by checkPhoneVerificationSmsCode, which requires smsVerificationCode/smsVerificationExpire in the blob) cannot consume the e-mail blob directly — it returns 501. (Social sign-up uses a separate 2-step pair, §5.6.4.)

5.6.1 Verify — auth.verifyRegistration

POST /patients/verifyAddingNewPatient. Not public — guarded by auth:apiusers, so it uses the SDK's partner token (the same apiusers bearer as partnerHealthInformation, no specific scope required here), plus a throttle:perHourFifty limit. This is the reason the step needs a configured partnerToken; a patient bearer will not satisfy the guard.

Request body: name, surname, phoneNumber, phone_code, email, password, passwordAgain, acceptUserAgreement, one of g-recaptcha-response-v2 / captcha, optional userAgreements[].

Rules (validated in VerifyAddingNewPatientRequest):

  • phoneNumber must match ^[+]([0-9\s\(\)]*)$ and be unique in mbl_users (this is where duplicate-account detection happens).
  • phone_code must match ^\+\d{1,3}$ (e.g. +90).
  • email is required (modern app versions) and unique in mbl_users.
  • passwordAgain must equal password; the SDKs auto-fill it from password.
  • CAPTCHA is mandatory (g-recaptcha-response-v2 or captcha, required-without each other), validated last via a live Cloudflare/Google call. A pure server-side caller cannot mint this token — it must come from a browser/human. The SDK method is therefore a thin passthrough: the caller supplies the captcha token.

Success → data: { response: "<hashedCode>", confirmationType: "sms" | "email" }. The SDK returns data verbatim; feed response (and the code the user receives) into register. The response blob is opaque and passed through unchanged (§8.2).

5.6.2 Confirm e-mail — auth.confirmRegistrationEmail

POST /patients/emailConfirmationRegister. Public (guarded by checkEmailVerificationCode + throttle). Called only when verifyRegistration returned confirmationType: "email".

Request body: verificationCode (the e-mailed code), response (the blob from verifyRegistration), optional userAgreements[]. The rest of the profile is carried inside the encrypted blob and merged server-side (prepareForValidation), so the SDK only sends these fields.

Success → data: { response: "<new SMS blob>", confirmationType: "sms" }. Feed that response + the SMS code into register.

5.6.3 Create — auth.register

POST /patients/addNewPatient. Public but guarded by SMS verification (checkPhoneVerificationSmsCode) + throttle.

Request body: name, surname, apiUserName, phoneNumber, password, smsVerificationCode, response (the SMS blob from the previous step), acceptUserAgreement (1), apiClientId, apiSecretKey.

Rules (validated):

  • phoneNumber must match ^[+]([0-9\s\(\)]*)$ — i.e. start with + and country code (e.g. +90 555 111 22 33). Bare digits are rejected.
  • apiUserName is used as the afterRegister token username; send the same +CC value as phoneNumber, otherwise auto-login mints a wrong/empty token.
  • Password is stored as Hash::make(BULUT_API_ENC_KEY . password) (bcrypt rounds=12).

Success → patient created + automatic afterRegister login → data: { access_token, refresh_token }.

5.6.4 Social sign-up — verifyRegistrationSocialregisterSocial

A separate public 2-step pair for users who authenticate via a social provider. Unlike verifyRegistration, both are public — no CAPTCHA and no partner token.

  • verifyRegistrationSocialPOST /patients/verifyAddingNewPatientSocial. Body: name, surname, phoneNumber, password, passwordAgain, socialType, key, optional email, acceptUserAgreement, userAgreements[]. Sends the SMS code → data: { response } (note: no confirmationType).
  • registerSocialPOST /patients/addNewPatientWithSocial (guarded by checkPhoneVerificationSmsCode). Body: smsVerificationCode, response (blob from the verify step), optional userAgreements[]; the profile is merged from the blob. Creates the social patient and its sec_social_login link row. Does not auto-login — obtain tokens afterwards with connect({ loginMode: "social" }).

5.7 Logout — auth.disconnect

POST /general/disconnectApi. Bearer required (auth:patients,apiusers,doctors). Revokes the current access + refresh tokens server-side. The SDK then clears the token store. Optional device-token fields (firebase/ios) may be added to the body.

5.8 Password reset — forgotPasswordresetPassword

A public 2-step self-service reset flow.

  • forgotPasswordPOST /patients/forgotPassword. Body: phoneNumber (must be a registered number), optional birthdate (YYYY-MM-DD; some installs verify it), and one of g-recaptcha-response-v2 / captchaCAPTCHA is mandatory outside the local environment (browser-minted, like verifyRegistration). Sends the SMS confirm code → data: { response }.
  • resetPasswordPUT /patients/forgotPassword (guarded by checkForgotPasswordConfirmSmsCode). Body: smsConfirmCode, response (blob from forgotPassword), password, passwordAgain. Sets the new password (keyed by the phone/birthdate carried in the blob). Terminal — returns a success message, no tokens.

6. Endpoint reference (48)

Notation: Canonical name = language-neutral concept → per-language naming follows §7. [public] = no auth; [bearer] = access token; [partner] = partner token; [scope:…] = required OAuth scope.

6.1 auth

Canonical Method Path Auth
connect POST /general/connectApi public
connectWithTwoFactor POST /general/connectApiWithTwoFactor public
refresh POST /general/refreshApi public
verifyRegistration POST /patients/verifyAddingNewPatient partner
confirmRegistrationEmail POST /patients/emailConfirmationRegister public
register POST /patients/addNewPatient public*
verifyRegistrationSocial POST /patients/verifyAddingNewPatientSocial public
registerSocial POST /patients/addNewPatientWithSocial public*
forgotPassword POST /patients/forgotPassword public
resetPassword PUT /patients/forgotPassword public
disconnect POST /general/disconnectApi bearer

(Bodies and responses in §5.)

6.2 doctors [bearer] [scope:patients,bulutweb]

Canonical Method Path Body / params
branches GET /patients/allBranches
locations GET /patients/allLocations
quickSearch POST /patients/quickSearch searchText (3–100, req), listType (interview|appointment|null), location (null)
search POST /patients/filteredSearch searchParams{}, orderParams[], otherParams[], currentPage (≥1, req), perPageLimit (10–100)
detail GET /patients/doctorDetail/{id}/{corporate?} path id (req), optional corporate
  • quickSearch response: { searchedBranches, searchedDoctors, searchedCompanies, searchedGivenTreatments, searchedBlogs, queryText }; each item { result_id, result_text, result_url, result_sub_text, result_type, result_image }.
  • search.searchParams keys: withFreeText, withDoctorName, withBranchName, withBranchId (-1 excludes psychology/diet), withLocationName, withLocationId, withCompanyName, withCompanyId, withGivenTreatments, withExpertyId, withInstitutionId, withNearestSlotDayRange. orderParams: name | point | slot | order. otherParams: isKizilay | isQuestionable | isInterviewable | isAppointmentable. Response: data: { foundDoctorsCount, foundDoctors: [ { doctor_id, name, surname, branch_name, star_rate, nearest_slot, isInterviewable, isAppointmentable, url, user_image, … } ] }.
  • detail returns doctorGeneralInfo (prices, session length, branch), education, languages, reviews, videos, special services, related clinics. The doctor_id here feeds later steps.

6.3 slots [bearer]

Canonical Method Path Body
schedule POST /patients/doctorScheduler doctorId (numeric, req); scheduleDate (Y-m-d, today..+21, optional); scheduleStep + schedulePage (window paging — both required when scheduleDate omitted); listType (req: interview → online slot_type 1,2; else physical slot_type 0,2)

Response: data = date-keyed map → for each date [ { slotId, slotStart "HH:mm:ss", slotEnd "HH:mm:ss", available: true } ]. Empty days are []. Next step's appointmentDate = "Y-m-d H:i" (date key + slotStart, drop seconds).

6.4 appointments [bearer] [scope:patients,bulutweb]

Canonical Method Path Body / params
reserveInterview POST /patients/addInterviewDateReservation doctorId (numeric, req), appointmentDate (Y-m-d H:i, today..+21, req), appointmentType (interview|appointment, default interview)
addPhysical POST /patients/addNewAppointment doctorId (numeric, req), appointmentDate (Y-m-d H:i, req). No appointmentType.
cancel DELETE /patients/deleteUserAppointment/{eventId} path eventId (= cln_events.id)
list GET /patients/userAppointments/{page?} optional path page (paging disabled — page 1 = full list)
reservations GET /patients/userReservations

reserveInterview success → { resultType: 0, data: null }; failure → 501. cancel → 501 for insurance appointments, past cancel-window, or not found. Slot is resolved server-side from doctorId + appointmentDate (no slotId in request).

  • listdata: { foundAppointmentsCount, foundAppointments: [ { event_id, event_start_date, doctor_id, doctor_name, doctor_surname, status, amount, online_call, … } ] }. event_id is the id cancel takes; rows with event_id == "0" are paid-order/refund entries (not cancellable) — filter them out.
  • reservations → a bare array of active online-slot holds: { appoinment_date, doctor_id, doctor_name, doctor_surname, medical_branch_name, minute_diff, second_diff } (pair minute_diff+second_diff for a countdown).

6.5 payments

Canonical Method Path Auth Notes
checkDiscountCode POST /patients/checkDiscountCode bearer patients prefix, not payments.
getCards GET /payments/getCards bearer
saveCard POST /payments/saveCard bearer Flat fields (not nested).
pay POST /payments/interviewPayment bearer Throttle 20/h/IP. Returns payment3DUrl.
deleteCard DELETE /payments/deleteCard/{cardId} bearer path cardId
  • checkDiscountCode body: checkType (question|appointment|lab|special|physicallyAppointment|tmcLab|program), doctorId (required except lab/tmcLab/program), discountCode (req), plus orderId/specialServiceId/programSlug per type. Valid → data: { discount_code, discount_title, discount_id, prices }.
  • getCardsdata.cards[]: { id, card_holder_name, card_number (masked), card_type, created_at }. idcardId.
  • saveCard body (flat, SavePatientCardRequest): cardHolder, cardNumber, cardExpMonth (m), cardExpYear (Y), cardCvv — all required.
  • pay body: doctorId (req), appointmentDate (Y-m-d H:i, req), appointmentType (interview→order_type 0 / appointment→3), is3D (bool, req), termsAccept (accepted, req), saveCard (1=tokenize), discountCode (opt), caseDetail (opt, encrypted), and either cardInfo{ cardHolder, cardNumber, cardExpMonth, cardExpYear, cardCvv } (all-or-none) or cardId (saved card). Amount is computed server-side (no amount in request).
  • pay response: see §8.1 (payment3DUrl handling).

6.6 measures

Patient endpoints [bearer] [scope:patients]; partner endpoint [partner] [scope:teusan]. Records are written to the authenticated patient (bas_com_company_id from token).

Canonical Method Path Body / params
addList POST /patients/addNewUserMeasures data[] — each item: type + that type's fields + date_time. Primary "submit health data" endpoint.
add POST /patients/addNewUserMeasures/{type} path type; body: date_time + type fields
update PUT /patients/updateUserMeasures/{type} path type; body: id (req) + fields + date_time
delete DELETE /patients/deleteUserMeasures/{type} path type; body: id (req)
last GET /patients/measuresList Latest value per type.
list GET /patients/userMeasuresList/{type}/{page}/{glucoseType?} path; glucoseType 0/1 only for glucose
graph GET /patients/userMeasuresGraph/{type}/{period}/{page}/{glucoseType?} period 1=day,2=week,3=month,4=year
partnerHealthInformation POST /outher/healthInformation partner token; body: identity, phoneNumber, data[]

addList runs in a DB transaction; submit multiple measurements in one call. last returns the most-recent of each type (tension splits into hypertension/hypotension; glucose splits into hunger_glucose/postprandial_glucose), each with a *Date.

Measure type schema (every record also requires date_time = "Y-m-d H:i"):

type Fields
tension hypertension (systolic), hypotension (diastolic)
glucose glucose, glucose_type (0=fasting, 1=postprandial)
pulse pulse
fever fever
weight weight (BMI auto-computed)
length length (BMI auto-computed)
waist waist
hip hip
fat fat
muscle muscle
calorie calorie
step step
sleep sleep (hours; stored to sleep_time)

Value rules: numeric; tension/pulse digits 1–10; glucose 0–99999.99 + glucose_type 0|1; weight/length 0–99999.99; etc.

Known API bug (document, don't replicate): for the partner endpoint, AddNewUserMeasuresListRequest::prepareForValidation reads identity from $this->message instead of $this->identity, nulling it during validation; in practice matching falls back to phoneNumber. The SDK sends the correct contract (identity + phoneNumber) and notes this in the README.

6.7 skin [bearer] [scope:patients]

"Cildimde Neyim Var" — AI skin-lesion analysis. Submit one or more skin photos; each is classified (lesion label), given a patient-friendly Turkish AI comment, image-quality flags, a confidence, possible ICD hints and an opaque case_detail blob.

Canonical Method Path Body
analyze POST /patients/imageCheck images[] — each item { image (base64, req), branch_id? }

Request: { "images": [ { "image": "<base64>", "branch_id"?: <int> } ] }. image is a base64-encoded JPEG/PNG/WebP/HEIC (a data:…;base64, prefix is accepted). branch_id optionally tags the stored media with a clinic branch. Mirrors measures.addList — a loose array of records.

Response data: { status: [ { id, isClear, isBright, label, comment, confidence, image, error, possible_icd, case_detail } ] } — one entry per submitted image, id = 1-based index:

  • label — lesion class from the classifier (may be empty).
  • comment — patient-friendly Turkish AI summary.
  • isClear / isBright — image-quality flags.
  • confidence — classifier confidence (0–1) or null.
  • image — stored media relative path.
  • possible_icd — candidate ICD code(s) or null.
  • case_detail — opaque base64-encrypted blob identifying the saved case (§8.2); can be forwarded verbatim as a payment's caseDetail.
  • error — per-image error message or null.

The SDK returns data verbatim (a mostly-untyped map) and never decrypts case_detail. On a gateway failure the API still returns status entries with empty label/comment, so callers should treat all fields as optional.

6.8 meals [bearer] [scope:patients]

AI meal-photo calorie/nutrition estimation — sibling of skin (same controller, different domain).

Canonical Method Path Body
analyze POST /patients/imageAnalyzeMeal image (base64, req), portionSize (req), portionGrams?, mealType (req), note?

The SDK input names map to the API's snake_case body { image, portion_size, portion_grams?, meal_type, note? } (like payments.pay, a typed single input):

  • portion_sizesmall | medium | large | custom (required).
  • portion_grams — required only when portion_size is custom.
  • meal_typebreakfast | lunch | dinner | snack (required).
  • note — optional free text (≤1000 chars); the model reads Turkish preparation/portion modifiers.

Response data: { status: { comment: "<json string>" } }comment is the model's nutrition breakdown (a JSON-object string, per the server prompt); the SDK returns it verbatim.

6.9 laboratory [bearer] [scope:patients]

The patient's own laboratory results, the orderable test catalog, and test pre-ordering. The controller (v3\General\Laboratory) is @hideFromAPIDocumentation, so this group is hand-written from the API source, not the auto-docs.

Canonical Method Path Body / params
results GET /patients/userLabTestList/{page?} optional page (default 1). The patient's completed/in-progress lab results.
resultDetail GET /patients/userLabTestDetail/{testId} path testId (string) — pass the id from a results item verbatim.
catalog GET /patients/allLaboratoryTests — (orderable test-group catalog).
catalogDetail GET /patients/laboratoryTestDetail/{id} path id (numeric) — one catalog group.
order POST /patients/addNewLaboratoryTest testId (numeric, req), addressId (numeric, req), laboratoryId (numeric, req).
  • results data: { foundTestsCount, foundTests: [ { id, created_at, company_name, test_name, test_state, test_state_text, test_type, test_type_text } ] }. test_state 0=Numune Alınıyor, 1=Çalışıyor, 2=Onaylandı; test_type 1=Normal, 2=Grup, 3=Alt Parametre. The list also unions in TMC-lab-ordered tests whose id carries a -lab suffix (e.g. "4821-lab").
  • resultDetail: {testId} may be a plain id ("123", DB path) or "<id>-lab" (TMC-lab path). data fields: test_name, protocol_no, id, created_at, company_name, result, result_unit, test_state, test_state_text, test_type, test_type_text, result_type_text, sub_tests. For test_type == 1 the payload adds normal_lower_limit, normal_upper_limit, panic_lower_limit, panic_upper_limit (age/gender-aware). For test_type == 2, sub_tests[] carries per-parameter results plus those four limit fields.
  • catalog returns test_groups[]: each { id, name, image, background, desc (HTML), tests[]{id,name}, laboratories[]{ company_id, name, doctor_id, branch_id, prices{ real_price, discount_rate, discounted_price, discount_code, discount_title, discount_id }, cities[]{id,name} } }. Served from config/laboratory.php, not the DB.
  • catalogDetail returns the single matching group; for pat users the per-laboratory prices are recomputed through the discount provider.
  • order success → data: { preOrderId }. Validated against the catalog (test_not_found, laboratory_not_found), the user address (user_address_not_found, invalid_user_address_for_lab — the address city must be served by the lab), and duplicate open orders. Business failures return HTTP 501.
  • Not exposed: the deprecated POST /patients/addNewLabTest (superseded by order), and the results endpoint's optional companyId query filter (SDKs use path params only, and that server-side filter is known-buggy).

6.10 diets [bearer] [scope:patients]

The patient's diet lists (a dietitian's "Diyet Listesi"). Controller v3\General\Diets, @hideFromAPIDocumentation. JSON only — the server's PDF export (dietFile) is out of SDK scope (it returns a binary application/pdf, not the envelope).

Canonical Method Path Body / params
list GET /patients/dietLists/{page?} optional page (default 1). Page size is fixed to 10 server-side.
detail GET /patients/diet/{listId} path listId (numeric) = a list_id from a list item.
  • list data: { foundDietsCount, foundDiets: [ { list_id, diet_date, protocol_no, patient_name, patient_surname, patient_birthdate, patient_identity_no, doctor_company_name, doctor_name, doctor_surname, doctor_title, doctor_branch_name, doctor_image } ] }. One entry per diet-program group; list_id feeds detail.
  • detail data: an array of meal-time groups [ { time, meals: [ { meal_time, total_calories, protocol_no, patient_*, doctor_company_name, diet_date, doctor_name, doctor_surname, doctor_title, doctor_image, doctor_certified_number, doctor_branch_name, meal_details: [ { quantity, explanation, meal_name, kcal, unit } ] } ] } ]. An empty diet returns HTTP 501.

6.11 addresses [bearer] [scope:patients]

The patient's saved addresses. Required by laboratory.order, whose addressId must reference one of these (and whose city_id must be in the lab's served cities). All four verbs are the same path /patients/userAddress (distinguished by method).

Canonical Method Path Body / params
list GET /patients/userAddress
add POST /patients/userAddress title (req), cityId (req, numeric), districtId (req, numeric), address (req), locationLat (req), locationLng (req), description?, isDefault? (0|1)
update PUT /patients/userAddress id (req); title/cityId/districtId/address/locationLat/locationLng are required_without:isDefault; description?, isDefault?
delete DELETE /patients/userAddress id (req, in the body — not a path segment)
  • list → a bare array (default first): { id, title, description, city_id, district_id, address, location_lat, location_lng, is_default, distinct_name }. id is the addressId used by update/delete/laboratory.order. Returns 501 when the patient has no addresses (treat as "empty"). Only the district name is joined — map city_id→name via doctors.locations.
  • adddata: { addressId }. The first address is forced default; setting isDefault: 1 demotes the previous default.
  • update/delete → message only (no data). The default address cannot be deleted (reassign via update first), nor can an address already used on an order.
  • cityId comes from doctors.locations (location_id); districtId comes from GET /getConfig (cities[].districts[].district_id), reachable via the §7.2 escape hatch.

7. Naming conventions & API shape

The client is a single root object exposing one accessor per service group; each group exposes the canonical methods above.

client.auth.connect(...)            client.payments.pay(...)
client.doctors.search(...)          client.measures.addList(...)
client.slots.schedule(...)          client.appointments.reserveInterview(...)
client.skin.analyze(...)            client.meals.analyze(...)
client.laboratory.results(...)      client.diets.list(...)

Per-language casing & idioms:

Language Method case Notes
JS/TS camelCase client.doctors.quickSearch(). Promise-based.
PHP camelCase $client->doctors->quickSearch(). Namespace Bulutklinik\Sdk.
Python snake_case client.doctors.quick_search(). Sync and async (AsyncClient).
Go PascalCase client.Doctors.QuickSearch(ctx, …). Context-first, (T, error) returns.
Java camelCase client.doctors().quickSearch(…). Builder for config; checked vs unchecked TBD in Faz 3.
C# PascalCase+Async client.Doctors.QuickSearchAsync(…). Task<T>, CancellationToken.
C++ snake_case client.doctors().quick_search(…). Namespace bulutklinik. cpr + nlohmann/json.

Request inputs are typed structures (objects/records/structs) per language; responses are typed where practical, otherwise a typed envelope + parsed data.

7.1 Client configuration

Option Default Purpose
environment / baseUrl production Named preset or explicit URL.
lang tr Default lang header; overridable per request.
clientId / clientSecret Needed for refresh (and passed by connect).
tokenStore in-memory Pluggable persistence.
timeout sane default Request timeout.
httpClient platform default Injectable transport (PSR-18, http.Client, HttpClient, etc.).

7.2 Escape hatch — arbitrary requests

Not every endpoint has a typed resource method, and the API grows faster than the SDK surface. Every SDK therefore exposes one generic request method on the root client for calling any Bulutklinik API endpoint directly. It is not a separate HTTP client: it reuses the same transport, so default headers, the chosen auth mode, silent token refresh + retry (§5.4), envelope unwrapping (§3) and the typed error hierarchy (§4) all still apply.

Concept:

client.request(method, path, { auth, body, lang }) -> data
Param Notes
method GET | POST | PUT | DELETE.
path Relative to the configured base URL, e.g. /patients/allBranches. Leading slash included.
auth public | bearer (default) | partner. Accepted as a string or an existing public enum/const per language.
body Optional JSON payload (object/map/dict). Omitted on GET.
lang Optional per-request lang override, where the SDK's transport supports one (JS, PHP, Go, C++). Python / Java / C# apply the client-level lang.

Returns the unwrapped data payload as the language's raw JSON value (the same type a future typed resource method would parse from), and raises the same typed errors on failure. Representative per-language signatures (idiomatic, return the raw data):

Language Signature
JS/TS client.request<T>({ method, path, auth?, body?, lang? }): Promise<T>
Python client.request(method, path, *, auth="bearer", body=None) — plus the async client
PHP $client->request(string $method, string $path, string $auth = 'bearer', ?array $body = null, ?string $lang = null): mixed
Go client.Do(ctx, method, path, *bk.RequestOptions) (json.RawMessage, error) (nil options ⇒ bearer)
Java client.request(String method, String path, String auth, Object body)JsonNode
C# client.RequestAsync(HttpMethod method, string path, string auth = "bearer", object? body = null, CancellationToken = default)JsonElement
C++ client.request(method, path, bulutklinik::RequestOptions{})nlohmann::json

This is the supported extension point for endpoints outside the 29 in §6. Prefer a typed resource method when one exists; reach for request only for the gaps.


8. Special cases

8.1 payment3DUrl (3-D Secure) — passthrough

pay success response: { resultType: 0, data: { payment3DUrl: "<url>" } }. payment3DUrl is a browser URL the SDK returns verbatim — it is one of: (A) the bank's direct URL_3DS, or (B) {APP_URL}/api/v3/payments/threeDUrl/<token> (our endpoint serving the 3DS HTML form).

The SDK does not open, follow, or parse it. 3DS completion ("provizyon kapatma" / capture) happens browser↔bank↔server via the POST /api/v3/threeD/appointmentPaymentComplete/{trxId}/{driver} callback (trxId = "{orderId}.{transactionUuid}.{processId}") — outside SDK scope. If is3D = false, data is the inline-completed order result (no payment3DUrl).

8.2 Encrypted blobs — passthrough

connect's data.response (2FA), register's response, caseDetail, and the case_detail returned by skin.analyze are opaque encrypted blobs. The SDK passes them through verbatim and never encrypts or decrypts — a skin.analyze case_detail may be forwarded unchanged as a payment's caseDetail. The clinic/API encryption keys are never embedded in the SDK.

8.3 Public vs bearer vs partner

  • Public (no Authorization): connect, connectWithTwoFactor, refresh, register, confirmRegistrationEmail, verifyRegistrationSocial, registerSocial, forgotPassword, resetPassword. (forgotPassword still requires a browser CAPTCHA token; the social pair does not.)
  • Bearer (access token): everything else (incl. appointments.* and the addresses group).
  • Partner: partnerHealthInformation (scope:teusan) and verifyRegistration (auth:apiusers, no specific scope) use the separately-configured partner token, not the patient access token. Only these two need the partner token — the social registration verify step is public, unlike the non-social verifyRegistration.

9. Cross-cutting requirements (every SDK)

  1. Idiomatic, hand-written — no codegen; match each ecosystem's conventions.
  2. Minimal dependencies — prefer the platform HTTP client; pin the documented stack per language (see §7 / PLAN.md).
  3. Typed — public API and data payloads typed where the language supports it.
  4. Auto-refresh + retry per §5.4, concurrency-safe.
  5. Pluggable token store and HTTP client.
  6. Errors per §4 with full context.
  7. Tested — unit tests for envelope/error/refresh logic + at least one live smoke path against test env (Faz 1–2).
  8. Examplesexamples/ with the end-to-end flow: login → search → slot → reserve → (pay) and a measures example.
  9. Self-contained repo — README, LICENSE (MIT), DESIGN.md copy, CI.
  10. Versioning — semver; tag vX.Y.Z per repo.

10. Live validation reference (test env)

  • Base: https://apitest.bulutklinik.com/api/v3
  • OAuth client: Patients_Web_Mobile — id 96b630b3-f62a-4e67-b33c-b58802dca5af (secret in the collection / env file).
  • Test patient: hackathon@bulutklinik.test (loginMode: email).
  • Bookable doctorId examples: 8282 (interview + physical), 168896 (interview).
  • Known env limits (request is correct, server/env is the cause):
    • quickSearch returns HTTP 404 / resultType 1 on test — the search driver (Elasticsearch) is unavailable there; the controller catches only QueryException so other exceptions surface as a generic 404. filteredSearch (doctors.search) works and is the production search path.
    • interviewPayment may 404 if POS isn't configured for the company; 3DS capture can't run from a non-browser client. SDK validation asserts the request shape + payment3DUrl return, not the bank capture.
  • TS reference live result (2026-06-17, test): 8/9 steps OK — auth.connect, doctors.branches (136), doctors.locations (81), doctors.search, doctors.detail, slots.schedule, measures.last, auth.disconnect all pass; only quickSearch fails for the env reason above.

11. Change control

This file is canonical. When it changes:

  1. Bump the spec version (§ top).
  2. Copy it into every language repo (<repo>/DESIGN.md).
  3. Reconcile each SDK against the change; note breaking changes in repo CHANGELOGs.

If an SDK must diverge from this spec, fix the spec first (or record the divergence here) — code and SSOT must never silently disagree.