A representative could create a doctor or clinic but not finish its profile:
PATCH /api/v1/doctor/{uuid} accepted only the doctor or an admin, and the
clinic gate ran through ClinicDoctorPermissionChecker, which asks about clinic
membership — a representative is not a member. Onboarding stopped at an empty
public record.
Grant is permanent while representation_id points at the rep, and limited to
content: RepresentationEditPolicy holds ownership plus the field whitelist.
Sending a key outside it aborts the whole request with 403 and names the field,
rather than filtering the payload silently, so a rep never believes a change
saved when it did not. medical_system_code, `active` and clinic `doctors` stay
out — credential, and membership, belong to the record's owner. `active` already
has a dedicated rep endpoint.
ClinicDoctorPermissionChecker is untouched on purpose; folding a second concept
into it would give it two reasons to change.
Doctor/clinic detail responses now carry can_edit, computed by the same policy
the PATCH gate uses, so the panel reads authorization instead of re-deriving it
and drifting. Both endpoints stay public: no token means can_edit false and an
otherwise unchanged payload, which is what nobat724_front consumes.
Address endpoints follow the same policy. createAddress now resolves its target
from an explicit doctor_uuid instead of findByUser first — a representative who
also has a doctor profile was silently writing the address onto their own.
Every rep edit writes one app_log row (channel representation_edit) recording
who, what, and which field names — never values. Owner and admin edits write
nothing, keeping /admin/logs readable.
Docs corrected where they already disagreed with the code: 403/404 error codes
on both PATCH routes, a non-existent "cannot delete the last clinic address"
409, and the missing gallery-size 422.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
29 KiB
Clinic API
Prefix:
/api/v1/clinic,/api/v1/clinics
POST /api/v1/clinic
Create a new clinic.
Permission: AUTH — any authenticated user becomes the clinic owner, and is
granted ROLE_CLINIC on success.
One clinic per user. ClinicRepository::findByUser() (which resolves the
caller's working context) is a findOneBy, so a second clinic would be
unreachable data — the request is rejected instead.
Errors:
| Code | HTTP | توضیح |
|---|---|---|
| ERR_VALIDATION_001 | 422 | name خالی است (field: name) |
| ERR_CONFLICT_001 | 409 | این کاربر از قبل مالک یک کلینیک است |
Request Body (application/json)
{
"name": "کلینیک الوند",
"info": "توضیحات کلینیک",
"address": "تهران، خیابان ولیعصر",
"telephone": "02112345678",
"working_days": "شنبه تا چهارشنبه",
"is_24_7": false,
"latitude": 35.6892,
"longitude": 51.3890,
"state": "تهران",
"city": "تهران",
"image_clinic": [
{ "url": "https://..." }
],
"clinic_logo": "https://...",
"doctors": ["uuid1", "uuid2"],
"specialties": [1, 2],
"doctor_services": [3, 4],
"insurance": [5, 6]
}
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | ✅ | Clinic name |
info |
string | ❌ | Description |
address |
string | ❌ | Full address |
telephone |
string | ❌ | Contact number |
working_days |
string | ❌ | Working days description |
is_24_7 |
boolean | ❌ | Open 24/7 flag |
latitude |
float | ❌ | Latitude for map |
longitude |
float | ❌ | Longitude for map |
state |
string | ❌ | Province name |
city |
string | ❌ | City name |
image_clinic |
object[] | ❌ | Gallery images [{url: "..."}] — max 5; more returns ERR_VALIDATION_001 (422) |
clinic_logo |
string | ❌ | Logo URL |
doctors |
string[] | ❌ | Doctor UUIDs to associate |
specialties |
integer[] | ❌ | Specialty IDs |
doctor_services |
integer[] | ❌ | Service IDs |
insurance |
integer[] | ❌ | Insurance IDs |
social_media |
object | ❌ | Social media URLs — keys: instagram, telegram, aparat, youtube, linkedin. Values are validated as URLs; invalid/empty values are stored as null. |
Response 201
{
"success": true,
"data": {
"data": {
"uuid": "550e8400-...",
"name": "کلینیک الوند",
"info": "...",
"address": "...",
"telephone": "02112345678",
"working_days": "...",
"is_24_7": false,
"latitude": 35.6892,
"longitude": 51.3890,
"state": "تهران",
"city": "تهران",
"images_clinic": [{ "url": "https://..." }],
"clinic_logo": "https://...",
"is_active": true,
"doctors": [],
"specialties": [],
"doctor_services": [],
"insurance": [],
"tags": [],
"created_at": 1717000000
}
}
}
⚠️ Double-nested: Frontend extracts with
data?.data?.data
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing or invalid token |
GET /api/v1/clinic/{uuid}
Get clinic detail.
Permission: PUBLIC — a token is optional and only affects can_edit.
Path Parameters
| Param | Type | Description |
|---|---|---|
uuid |
string (UUID) | Clinic UUID |
can_edit
The payload carries can_edit: boolean — whether the caller may PATCH this clinic. It is true
for the owner, ROLE_ADMIN, a member doctor holding clinic_info.update, and the registering
representative; false for everyone else and always false without a token. It is computed by the
same checks the PATCH gate uses, so clients must read it rather than re-deriving the rule.
It says nothing about which fields are writable — a representative sees can_edit: true yet is still
limited to the whitelist under PATCH /api/v1/clinic/{uuid}.
Response 200
{
"success": true,
"data": {
"data": {
"id": "233",
"uuid": "550e8400-...",
"name": "کلینیک الوند",
"title": "کلینیک الوند",
"is_active": true,
"phone": "02112345678",
"phone_number": "02112345678",
"logo": "/uploads/clinics/logo/...",
"clinic_logo": "/uploads/clinics/logo/...",
"images_clinic": [{ "url": "/uploads/clinics/gallery/..." }],
"social_media": {
"instagram": "https://instagram.com/clinic.example",
"telegram": "https://t.me/clinic_example",
"aparat": null,
"youtube": null,
"linkedin": null
},
"caption": "توضیحات کلینیک",
"list_bime": [],
"specialties": [{ "uuid": "...", "id": "1", "name": "قلب", "parent": null }],
"services": [],
"clinic_specialty": [{ "uuid": "...", "id": "1", "name": "قلب", "parent": null }],
"doctors": 5,
"doctor_list": null,
"city": [{ "uuid": "...", "id": "132", "name": "یزد", "parent": "100" }],
"state": [{ "uuid": "...", "id": "100", "name": "یزد" }],
"location": "یزد، خیابان اصلی، پلاک 101",
"map": { "latitude": "31.868", "longitude": "54.330" },
"24_7": false,
"field_working_days": "شنبه تا پنجشنبه ۸ تا ۱۸"
}
}
}
city/state/map/location/phone/phone_numberare all resolved from the clinic's address (DoctorAddresslinked byclinic_id), not from columns on the clinic.locationandphone/phone_numberfall back to the deprecatedclinics.address/clinics.telephonecolumns only when the address record has no value — reading them from different rows made one response describe two different places. Each is an array with a single object (or empty[]if the clinic has no address).doctorsis a count; the actual doctor list comes fromGET /api/v1/clinic/doctor-list/{clinicUuid}(doctor_listhere is alwaysnull).
معنای is_active
is_active: false یعنی «موقتاً غیرفعال»، نه «حذفشده». تصمیم صریح، چون رکورد و نوبتهای تاریخیاش باقی میمانند و کلینیک ممکن است دوباره فعال شود.
پیامدها:
- کلینیک غیرفعال همچنان از API برمیگردد و لینک مستقیمش ۲۰۰ میدهد (نه ۴۰۴/۴۱۰) تا لینکهای موجود نشکنند.
- سایت عمومی همان صفحه را
noindexمیکند و از sitemap بیرون میگذارد (nobat724_front/lib/entityQuality.js→isThinClinic). - اگر روزی معنای «حذفشده» لازم شد، باید فیلد جداگانهای اضافه شود — نه بازتعریف این یکی.
نام کلینیک
name نمیتواند شمارهتلفن یا مقدار آزمایشی (test، تست، -) باشد؛ این مقادیر با 422 رد میشوند (App\Shared\Util\DisplayName). null مجاز است و یعنی «هنوز نامگذاری نشده».
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_NOT_FOUND_001 |
404 | Clinic not found |
ERR_VALIDATION_001 |
422 | نام کلینیک شمارهتلفن یا مقدار آزمایشی است |
PATCH /api/v1/clinic/{uuid}
Update a clinic.
Permission: AUTH — the clinic owner, ROLE_ADMIN, a member doctor holding clinic_info.update
(see Clinic Doctor Permissions), or the registering representative (a ROLE_REPRESENTATION
user whose Representation.id equals the clinic's representation_id).
The representative's grant is permanent for as long as representation_id points at them, but it is
restricted to content fields — see Representative field whitelist below. Owner, admin and member-doctor
access is unchanged and unrestricted. The representative path deliberately bypasses
ClinicDoctorPermissionChecker: that class answers "is this doctor a member of this clinic", and a
representative is not a member at all.
Path Parameters
| Param | Type | Description |
|---|---|---|
uuid |
string (UUID) | Clinic UUID |
Representative field whitelist
A representative may send only these keys. Any other key aborts the whole request with 403
and nothing is saved — the payload is not silently filtered.
name · info · address · telephone · working_days · 24_7 · latitude · longitude ·
practice_domain_uuid · state · city · social_media · image_clinic · clinic_logo ·
specialties · doctor_services · insurance
Notably excluded: doctors. Which doctors belong to a clinic is a membership decision for the owner,
not for the representative who registered it. specialties / doctor_services / insurance are
allowed — they are the clinic's public-facing catalogue, the mirror of the same keys on the doctor
whitelist, and are not membership.
Every successful representative edit writes one app_log row with channel = 'representation_edit',
recording the representative id, the target uuid, and the names of the changed fields (never
their values). Edits by the owner, a member doctor, or an admin write no such row.
Request Body
Same fields as POST — all optional — plus:
| Field | Type | Description |
|---|---|---|
practice_domain_uuid |
string (UUID) | "" | null |
حوزهٔ فعالیت کلینیک. رشتهٔ خالی یا null یعنی «پاک کن»؛ نبودنِ کلید یعنی «دست نزن». uuid ناشناس ۴۲۲ میگیرد، نه رد شدن بیصدا. ← practice-domain.md |
Response 200
Updated clinic object (same structure as GET). Carries practice_domain — the full domain object, or
null when unset:
{"uuid":"8c7bfd18-9159-11f1-b98b-f28fd8aa5db5","code":"beauty","name":"کلینیک زیبایی","sort_order":0,"active":true}
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing token |
ERR_FORBIDDEN_001 |
403 | Secretary without clinic_info.update — thrown by the pre-check, before the clinic is even loaded |
ERR_AUTH_006 |
403 | Not the owner, not an admin, not a permitted member doctor, and not the registering representative |
ERR_AUTH_006 |
403 | Representative sent a field outside the whitelist — the offending key is in errors[0].field |
ERR_VALIDATION_002 |
404 | Clinic not found |
ERR_VALIDATION_001 |
422 | image_clinic بیش از ۵ عکس دارد |
ERR_VALIDATION_002 |
422 | practice_domain_uuid به هیچ حوزهای اشاره نمیکند |
Real response for a whitelist violation (captured from a functional test run, not hand-written):
{
"success": false,
"data": null,
"errors": [
{
"code": "ERR_AUTH_006",
"message": "نماینده اجازهٔ تغییر این فیلد را ندارد",
"field": "doctors"
}
]
}
GET /api/v1/clinics
List clinics with pagination.
Permission: PUBLIC
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
page |
integer | ❌ | Default: 1 |
limit |
integer | ❌ | Default: 20 |
name |
string | ❌ | Search by clinic name |
city |
integer | ❌ | City id — filters by the clinic address's city |
state |
integer | ❌ | Province id — filters by the clinic address's province |
specialty |
integer | ❌ | Specialty id |
domain |
string | ❌ | دامنهی سایتِ درخواستکننده. اگر دامنهی یک نماینده سراسری باشد، فقط کلینیکهای همان نماینده برمیگردند و city/state نادیده گرفته میشوند؛ دامنه شهری/ناشناخته اثری ندارد |
city/stateare matched against the clinic's address (DoctorAddresslinked byclinic_id), not a field on the clinic itself.
Response 200
{
"success": true,
"data": [
{
"id": "3426",
"uuid": "...",
"name": "کلینیک الوند",
"title": "کلینیک الوند",
"phone": "02112345678",
"phone_number": "02112345678",
"logo": "/uploads/clinics/logo/...",
"clinic_logo": "/uploads/clinics/logo/...",
"images_clinic": [{ "url": "/uploads/clinics/gallery/..." }],
"doctors_count": 4,
"is_active": true,
"created_at": 1781762386,
"city": "تهران",
"state": "تهران",
"specialties": [{ "uuid": "...", "id": "7", "name": "..." }],
"24_7": false,
"field_working_days": "شنبه تا پنجشنبه ۸ تا ۲۰"
}
],
"meta": {
"totalRecords": 30,
"totalPages": 2,
"currentPage": 1
}
}
| Field | Type | Description |
|---|---|---|
clinic_logo / logo |
string|null | Logo path (relative /uploads/... or absolute URL) |
doctors_count |
integer | Number of doctors linked to the clinic |
city |
string|null | City name, resolved from the clinic's address (DoctorAddress) |
state |
string|null | Province name, resolved from the clinic's address (DoctorAddress) |
phone / phone_number |
string|null | Contact number from the clinic's address (DoctorAddress), falling back to the deprecated clinics.telephone column |
24_7 |
boolean | Open 24/7 flag |
field_working_days |
string|null | Working days/hours description |
GET /api/v1/clinic/doctor-list/{clinicUuid}
Get doctors associated with a clinic.
Permission: PUBLIC
Path Parameters
| Param | Type | Description |
|---|---|---|
clinicUuid |
string (UUID) | Clinic UUID |
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
page |
integer | ❌ | Default: 1 |
limit |
integer | ❌ | Default: 10, max 50 |
name |
string | ❌ | Filter by doctor name (LIKE) |
specialty |
integer | ❌ | Specialty id |
gender |
string | ❌ | man / woman |
degree |
string | ❌ | expert / general / specialist / subspecialistplus |
active |
0|1 | ❌ | Only doctors with appointments enabled |
sort |
string | ❌ | ASC / DESC by rating (default DESC) |
Filters apply only within this clinic's linked doctors.
⚠️ Double-nested: the doctors array is at
data.data(extract withdata?.data?.data); pagination is atdata.meta.
Response 200
{
"success": true,
"data": {
"data": [
{
"id": "1207",
"uuid": "...",
"name": "آرمان رضایی",
"gender": "man",
"degree": "specialist",
"img": [],
"specialties": [{ "uuid": "...", "id": "2", "name": "داخلی عمومی" }],
"satisfaction": "96",
"point": "4.8",
"free_turn": "پنجشنبه 09:00–13:00",
"hours_of_work": "شنبه تا چهارشنبه | پنجشنبه",
"active": true,
"city": [
{ "uuid": "7bfb989e-...", "id": "123", "name": "یاسوج", "parent": "23" }
],
"state": [
{ "uuid": "7bfb5705-...", "id": "23", "name": "کهگیلویه و بویراحمد" }
]
}
],
"meta": { "totalRecords": 3, "totalPages": 1, "currentPage": 1 }
}
}
| Field | Type | Description |
|---|---|---|
free_turn |
string | Next available appointment (e.g. پنجشنبه 09:00–13:00), or نوبت آزادی موجود نیست if the doctor has no active weekly schedule |
hours_of_work |
string | Working-days summary, or برنامه کاری تنظیم نشده when unscheduled |
active |
boolean | true only when appointments are enabled and the doctor has an active schedule |
city / state |
array | مکان خودِ پزشک (آدرس شخصی، و در نبودش آدرس کلینیک). آرایه با حداکثر یک عضو؛ پزشک بدون آدرس []. جزئیات و قاعدهٔ انتخاب در doctor.md |
free_turn/hours_of_work/activeare computed from each doctor'sWeeklySchedule(loaded in bulk by the endpoint). Without a schedule they fall back to the "not set" values.
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_NOT_FOUND_001 |
404 | Clinic not found |
DELETE /api/v1/admin/clinic/{clinicUuid}/doctor/{doctorUuid}
Detach a doctor from a clinic. This removes the clinic↔doctor link (the clinic_doctors association) and the doctor's clinic_doctor_permissions row; it does not delete the doctor or change the doctor's own active appointment flag.
Permission: AUTH — the caller must be ROLE_ADMIN or the owner of this clinic (ROLE_CLINIC whose user owns clinicUuid). Any other authenticated user gets 403.
Path Parameters
| Param | Type | Description |
|---|---|---|
clinicUuid |
string (UUID) | Clinic UUID |
doctorUuid |
string (UUID) | Doctor UUID |
Response 200
{ "success": true, "data": { "message": "پزشک از کلینیک جدا شد" } }
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_ACCESS_DENIED |
403 | Caller is neither an admin nor the clinic owner |
ERR_VALIDATION_002 |
404 | Clinic not found |
ERR_NOT_FOUND_001 |
404 | Doctor not found, or doctor not linked to this clinic |
Clinic Doctor Permissions
Each doctor attached to a clinic has a permission envelope scoped to that clinic only — the doctor's own practice is never affected. Rows live in clinic_doctor_permissions (one per clinic+doctor) and are created lazily with defaults for doctors who joined before this feature existed.
مجموعهٔ منابع را App\Shared\Security\PermissionCatalog تعیین میکند و از GET /api/v1/permission-catalog هم خوانده میشود — permission.md. این کلاس با منشی مشترک است، پس هر دو نقش دقیقاً یک فهرست از منابع و اکشنها دارند؛ فقط پیشفرضها فرق میکنند. تا پیش از این services برای پزشکِ عضو فقط view/update داشت و create/delete اصلاً قابل ذخیره نبود.
منبعی که بعد از ساختِ یک ردیف به رجیستری اضافه شود، هنگام خواندن پیشفرضِ نقش را میگیرد نه false، پس migration داده لازم نیست. توجه: حذفِ یک کلید از JSON یعنی «پیشفرض را بگیر»، نه «ممنوع» — برای ممنوعکردن باید false صریح ذخیره شود.
The envelope is always returned in full ({version, resources}); it is never flattened.
{
"version": 1,
"resources": {
"appointments": { "view": true, "create": true, "cancel": true, "update_status": true },
"patients": { "view": true, "create": true, "update": true, "delete": false },
"treatment": { "view": true, "update": true },
"payments": { "view": true, "create": false, "update": false, "delete": false },
"insurances": { "view": true, "create": false, "update": false, "delete": false },
"addresses": { "view": true },
"clinic_info": { "view": true, "update": false },
"services": { "view": true, "create": false, "update": false, "delete": false },
"inventory": { "view": false, "create": false, "update": false, "delete": false },
"staff": { "view": false, "create": false, "update": false, "delete": false },
"tags": { "view": false, "create": false, "update": false, "delete": false },
"discounts": { "view": false, "create": false, "update": false, "delete": false },
"sms": { "view": false, "create": false, "update": false, "delete": false },
"appointment_settings": { "view": true, "update": true },
"resources": { "view": true, "create": true, "update": true, "delete": true },
"clinic_doctors": { "view": false, "create": false, "update": false, "delete": false },
"subscription": { "view": false, "create": false }
}
}
active: false revokes everything at once regardless of the individual flags. The clinic owner and ROLE_ADMIN bypass all checks and can never be locked out.
Unknown resources and unknown actions in a PATCH body are silently ignored (اعتبارسنجی از PermissionCatalog::filterPatch)، so a client cannot invent permission keys. subscription و clinic_doctors حالا در رجیستری هستند ولی پیشفرضشان برای پزشکِ عضو خاموش است — عملیاتِ مالکِ کلینیکاند.
خواندنِ پرونده و تگ. PatientController::resolveScope و TenantTagController::guardTagView تا پیش از این فقط منشی را بررسی میکردند، پس پزشکِ عضو با patients.view خاموش بهجای 403 یک 200 با فهرست خالی میگرفت (نشتی نبود — tenant scoping خالیاش میکرد — ولی یک مجوز دو رفتار داشت). حالا هر دو نقش قرینهاند. تگها مثل قبل با tags.view یا patients.view باز میشوند، برای هر دو نقش.
اعمال (enforcement): همهٔ منابع در بکاند enforce میشوند. نقطهٔ واحد App\Clinic\Security\ClinicDoctorAccessChecker (denyUnlessGranted / memberClinicId) که فقط پزشکِ عضوِ کلینیک در محیطِ فعالِ کلینیک را محدود میکند؛ مالک/ادمین/منشی/پزشکِ مطبِ شخصی دستنخورده عبور میکنند. کنترلرهایی که tenant را نقشمحور حل میکنند (Inventory/Tag/Staff/Discount/Sms) با memberClinicId پزشکِ عضو را به دادهٔ کلینیک میبرند (نه مطبِ شخصی). نبودِ مجوز → 403. در پنل، سایدبار/Route/دکمههای CRUD با usePermissions().can برای محیطِ scope=clinic گِیت میشوند.
GET /api/v1/admin/clinic/{clinicUuid}/doctor-permissions
List the permission rows of every doctor in the clinic.
Permission: AUTH — clinic owner or ROLE_ADMIN
Response 200
{
"success": true,
"data": [
{
"uuid": "ce200cde-826d-11f1-b923-c282b864cdcc",
"clinic_uuid": "41e325c4-e825-4067-8438-5d828ecaee09",
"doctor_uuid": "bcabb3a8-cae3-45ec-876c-548f9c1e1569",
"doctor_name": "تست",
"active": true,
"permissions": { "version": 1, "resources": { "...": {} } },
"created_at": 1784352916,
"updated_at": 1784352916
}
]
}
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing token |
ERR_ACCESS_DENIED |
403 | Neither admin nor the clinic owner |
ERR_NOT_FOUND_001 |
404 | Clinic not found |
GET /api/v1/admin/clinic/{clinicUuid}/doctor/{doctorUuid}/permissions
Read one doctor's permissions. Creates the row with defaults if it does not exist yet.
Permission: AUTH — clinic owner or ROLE_ADMIN
Path Parameters
| Param | Type | Description |
|---|---|---|
clinicUuid |
string (UUID) | Clinic UUID |
doctorUuid |
string (UUID) | Doctor UUID — must already be attached to this clinic |
Response 200
Single permission object (same shape as one item of the list above).
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing token |
ERR_ACCESS_DENIED |
403 | Neither admin nor the clinic owner |
ERR_NOT_FOUND_001 |
404 | Clinic not found, or doctor not attached to this clinic |
PATCH /api/v1/admin/clinic/{clinicUuid}/doctor/{doctorUuid}/permissions
Update one doctor's permissions. Deep merge — only the resources/actions present in the body change; everything else keeps its current value.
Permission: AUTH — clinic owner or ROLE_ADMIN
Request Body
{
"permissions": { "resources": { "payments": { "create": true } } },
"active": true
}
| Field | Type | Required | Description |
|---|---|---|---|
permissions |
object | ❌ | {resources: {<resource>: {<action>: bool}}}. The bare {<resource>: {...}} form is also accepted. |
active |
bool | ❌ | false revokes all access to this clinic |
Response 200
Updated permission object.
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing token |
ERR_ACCESS_DENIED |
403 | Neither admin nor the clinic owner |
ERR_NOT_FOUND_001 |
404 | Clinic not found, or doctor not attached to this clinic |
ERR_VALIDATION_001 |
422 | permissions is not an object |
POST /file/upload/clinic_pro/clinic/field_clinic_logo
Upload clinic logo.
Permission: AUTH
Request
Content-Type: multipart/form-data
| Field | Type | Required | Max Size |
|---|---|---|---|
file |
binary | ✅ | 5MB |
Response 200
{
"success": true,
"data": {
"url": "https://clinic-pro.ddev.site/uploads/clinic/logo_abc.jpg",
"uuid": "...",
"filename": "logo_abc.jpg",
"filemime": "image/jpeg",
"filesize": 102400
}
}
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_FILE_001 |
422 | Invalid file type |
ERR_AUTH_001 |
401 | Missing token |
POST /file/upload/clinic_pro/clinic/field_image_clinic
Upload clinic gallery image.
Permission: AUTH
Request
Content-Type: multipart/form-data
| Field | Type | Required | Max Size |
|---|---|---|---|
file |
binary | ✅ | 5MB |
Response 200
{
"success": true,
"data": {
"url": "https://clinic-pro.ddev.site/uploads/clinic/gallery_abc.jpg",
"uuid": "...",
"filename": "gallery_abc.jpg",
"filemime": "image/jpeg",
"filesize": 307200
}
}
After uploading, use the returned
urlinsideimage_clinic: [{ "url": "..." }]when calling PATCH clinic.
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_FILE_001 |
422 | Invalid file type |
ERR_AUTH_001 |
401 | Missing token |
Clinic Address Management
GET /api/v1/clinic/{clinicUuid}/addresses
Permission: Public
Returns all addresses registered for a clinic (type=clinic entries).
Response 200
{
"success": true,
"data": [
{
"id": "12",
"uuid": "abc-123",
"type": "clinic",
"clinic_id": "5",
"name": "شعبه مرکزی",
"address": "تهران، خیابان ولیعصر...",
"telephone": "02112345678",
"map": { "latitude": "35.699", "longitude": "51.337" },
"city": { "id": "1", "name": "تهران" },
"province": { "id": "8", "name": "تهران" }
}
]
}
POST /api/v1/clinic/{clinicUuid}/address
Permission: Clinic owner, ROLE_ADMIN, or the clinic's registering representative
Creates a new address for the clinic. The address will appear in available-locations for doctors belonging to this clinic.
A clinic may hold only one address — posting a second one returns
409(ERR_CONFLICT_001). Use PATCH to change it.
Request
{
"name": "شعبه مرکزی",
"address": "تهران، خیابان ولیعصر...",
"telephone": "02112345678",
"latitude": 35.699,
"longitude": 51.337,
"city_id": 123,
"province_id": 7
}
| Field | Type | Required |
|---|---|---|
name |
string | ❌ |
address |
string | ❌ |
telephone |
string | ❌ |
latitude |
float | ❌ |
longitude |
float | ❌ |
city_id |
integer | ❌ |
province_id |
integer | ❌ |
Response 201
{ "success": true, "data": { "id": "12", "uuid": "...", "type": "clinic", ... } }
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_006 |
403 | Not the clinic owner |
ERR_VALIDATION_002 |
404 | Clinic not found |
PATCH /api/v1/clinic/{clinicUuid}/address/{addressUuid}
Permission: Clinic owner, ROLE_ADMIN, or the clinic's registering representative
Updates an existing clinic address. Same body fields as POST (all optional). The address is content, not membership, so no field whitelist applies to representatives here.
Response 200
{ "success": true, "data": { ... } }
DELETE /api/v1/clinic/{clinicUuid}/address/{addressUuid}
Permission: Clinic owner, ROLE_ADMIN, or the clinic's registering representative
Deletes a clinic address. There is no "must keep at least one" guard — the last address can be deleted.
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_VALIDATION_002 |
404 | Address or clinic not found |
ERR_AUTH_006 |
403 | Not the owner, not an admin, and not the registering representative |
Removed endpoint
POST /api/v1/clinic-pro/doctor-address/from-clinic/{clinicUuid} — removed. Use clinic address management endpoints instead.