{"info":{"name":"Maqsed API","description":"One API for the three Maqsed clients: the customer interface of the app (`/v1/customer`), the advertiser interface of the app (`/v1/advertiser`), and the admin control panel (`/v1/admin`).\n\n**Authentication.** Each namespace has its own opaque session token, sent as `Authorization: Bearer <token>`. Tokens are revocable server-side; a token from one namespace is rejected by the others. Routes without a lock are public: guests can call them, and some recognise a signed-in caller too.\n\n**Errors.** Every error has the same shape: `{ code, message, details?, requestId }`. Branch on `code`, which is stable; `message` is translated for display. For `VALIDATION_FAILED`, `details` lists each field problem as `{ field, code, message }`, where `code` is the rule that failed (e.g. `isSaudiMobileRequired` for an empty mobile, `isSaudiMobile` for an invalid one).\n\n**Mobile numbers** are accepted as typed (`05XXXXXXXX`, `+9665…`, Arabic-Indic digits) and returned in E.164 (`+9665XXXXXXXX`).\n\n**Language.** Messages are Arabic by default. Send `Accept-Language: en` (or `?lang=en`) for English. When signed in, the account's saved language is used if the request names none. Error responses carry `Content-Language`.\n\n**Request IDs.** Every response has an `X-Request-Id` header; quote it when reporting a problem.","schema":"https://schema.getpostman.com/json/collection/v2.1.0/collection.json"},"variable":[{"key":"baseUrl","value":"https://www.maqsed-api.dev-moltaqa.cloud","description":"API origin, without /v1."},{"key":"customerToken","value":"","description":"Session token for /v1/customer; filled by its sign-in request."},{"key":"advertiserToken","value":"","description":"Session token for /v1/advertiser; filled by its sign-in request."},{"key":"adminToken","value":"","description":"Session token for /v1/admin; filled by its sign-in request."},{"key":"lang","value":"ar","description":"Accept-Language: ar or en."}],"item":[{"name":"Customer","auth":{"type":"bearer","bearer":[{"key":"token","value":"{{customerToken}}","type":"string"}]},"item":[{"name":"Cities","item":[{"name":"List cities","request":{"method":"GET","description":"The active cities, for city pickers (registration, listings, filters), each with its region, sorted by name in the request language. Public. Send the chosen `id`; an inactive or unknown city is refused with `CITY_NOT_AVAILABLE`.","header":[{"key":"Accept-Language","value":"{{lang}}"}],"url":{"raw":"{{baseUrl}}/v1/customer/cities","host":["{{baseUrl}}"],"path":["v1","customer","cities"]},"auth":{"type":"noauth"}}}]},{"name":"Auth","item":[{"name":"Send a registration code","request":{"method":"POST","description":"Checks the registration form, then sends a 4-digit code to the mobile. Nothing is saved yet: send the same form again with the code to `register`. `MOBILE_ALREADY_REGISTERED` means the app should offer login. Call again to resend once `resendAfterSeconds` have passed.","header":[{"key":"Accept-Language","value":"{{lang}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/v1/customer/auth/register/otp","host":["{{baseUrl}}"],"path":["v1","customer","auth","register","otp"]},"body":{"mode":"raw","raw":"{\n  \"name\": \"سارة أحمد\",\n  \"mobile\": \"0551234567\",\n  \"email\": \"sara@example.com\",\n  \"acceptTerms\": true\n}","options":{"raw":{"language":"json"}}},"auth":{"type":"noauth"}}},{"name":"Register with the code","request":{"method":"POST","description":"The registration form again, with the code: creates the account and starts a session (the app then shows Home).","header":[{"key":"Accept-Language","value":"{{lang}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/v1/customer/auth/register","host":["{{baseUrl}}"],"path":["v1","customer","auth","register"]},"body":{"mode":"raw","raw":"{\n  \"name\": \"سارة أحمد\",\n  \"mobile\": \"0551234567\",\n  \"email\": \"sara@example.com\",\n  \"acceptTerms\": true,\n  \"code\": \"1234\",\n  \"deviceName\": \"iPhone 16\"\n}","options":{"raw":{"language":"json"}}},"auth":{"type":"noauth"}},"event":[{"listen":"test","script":{"type":"text/javascript","exec":["// Saves the new session token for the requests that follow.","if (pm.response.code >= 200 && pm.response.code < 300) {","  const token = pm.response.json()[\"accessToken\"];","  if (token) pm.collectionVariables.set(\"customerToken\", token);","}"]}}]},{"name":"Send a login code","request":{"method":"POST","description":"Sends a 4-digit code to a registered, active number. `MOBILE_NOT_REGISTERED` means the app should offer registration.","header":[{"key":"Accept-Language","value":"{{lang}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/v1/customer/auth/login/otp","host":["{{baseUrl}}"],"path":["v1","customer","auth","login","otp"]},"body":{"mode":"raw","raw":"{\n  \"mobile\": \"0551234567\"\n}","options":{"raw":{"language":"json"}}},"auth":{"type":"noauth"}}},{"name":"Log in with the code","request":{"method":"POST","description":"Checks the login code and starts a session.","header":[{"key":"Accept-Language","value":"{{lang}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/v1/customer/auth/login","host":["{{baseUrl}}"],"path":["v1","customer","auth","login"]},"body":{"mode":"raw","raw":"{\n  \"mobile\": \"0551234567\",\n  \"code\": \"1234\",\n  \"deviceName\": \"iPhone 16\"\n}","options":{"raw":{"language":"json"}}},"auth":{"type":"noauth"}},"event":[{"listen":"test","script":{"type":"text/javascript","exec":["// Saves the new session token for the requests that follow.","if (pm.response.code >= 200 && pm.response.code < 300) {","  const token = pm.response.json()[\"accessToken\"];","  if (token) pm.collectionVariables.set(\"customerToken\", token);","}"]}}]},{"name":"Log out","request":{"method":"POST","description":"Ends the session of the token sent; it stops working immediately. Other devices stay signed in.","header":[{"key":"Accept-Language","value":"{{lang}}"}],"url":{"raw":"{{baseUrl}}/v1/customer/auth/logout","host":["{{baseUrl}}"],"path":["v1","customer","auth","logout"]}}}]},{"name":"Account","item":[{"name":"My account","request":{"method":"GET","header":[{"key":"Accept-Language","value":"{{lang}}"}],"url":{"raw":"{{baseUrl}}/v1/customer/account","host":["{{baseUrl}}"],"path":["v1","customer","account"]}}},{"name":"Change my language","request":{"method":"PATCH","description":"The \"Change language\" screen. Requests without `Accept-Language` or `?lang=` are answered in it from the next request, and SMS messages use it.","header":[{"key":"Accept-Language","value":"{{lang}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/v1/customer/account","host":["{{baseUrl}}"],"path":["v1","customer","account"]},"body":{"mode":"raw","raw":"{\n  \"language\": \"en\"\n}","options":{"raw":{"language":"json"}}}}},{"name":"Delete my account","request":{"method":"DELETE","description":"Call after the app's confirmation dialog; there is no SMS code. Your details and files are removed, every device is signed out (this one included), and your number, email (and CR number) can register a new, empty account.","header":[{"key":"Accept-Language","value":"{{lang}}"}],"url":{"raw":"{{baseUrl}}/v1/customer/account","host":["{{baseUrl}}"],"path":["v1","customer","account"]}}},{"name":"Send a code to a new number","request":{"method":"POST","description":"Step 1 of changing the mobile number: the code goes to the new number. Call again to resend once `resendAfterSeconds` have passed.","header":[{"key":"Accept-Language","value":"{{lang}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/v1/customer/account/mobile/otp","host":["{{baseUrl}}"],"path":["v1","customer","account","mobile","otp"]},"body":{"mode":"raw","raw":"{\n  \"mobile\": \"0551234567\"\n}","options":{"raw":{"language":"json"}}}}},{"name":"Change my mobile number","request":{"method":"POST","description":"Step 2: checks the code sent to the new number and swaps it in; the old number no longer signs in. Every other device is signed out; this one stays signed in. A wrong code keeps the old number.","header":[{"key":"Accept-Language","value":"{{lang}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/v1/customer/account/mobile","host":["{{baseUrl}}"],"path":["v1","customer","account","mobile"]},"body":{"mode":"raw","raw":"{\n  \"mobile\": \"0551234567\",\n  \"code\": \"1234\"\n}","options":{"raw":{"language":"json"}}}}}]},{"name":"Files","item":[{"name":"Upload a file","request":{"method":"POST","description":"Send the file as multipart/form-data in the `file` field. Its type is checked from its bytes, not its name. Then pass the returned `id` to the endpoint that uses it; files unused after 24 hours are deleted.\n\n- `customer_avatar`: PNG, JPG, up to 5 MB, public\n- `chat_image`: PNG, JPG, WEBP, up to 5 MB, private\n- `chat_file`: PDF, up to 10 MB, private\n- `chat_voice`: M4A, AAC, up to 3 MB, private","header":[{"key":"Accept-Language","value":"{{lang}}"}],"url":{"raw":"{{baseUrl}}/v1/customer/files?purpose=customer_avatar","host":["{{baseUrl}}"],"path":["v1","customer","files"],"query":[{"key":"purpose","value":"customer_avatar","disabled":false}]},"body":{"mode":"formdata","formdata":[{"key":"file","type":"file"}]}}}]}]},{"name":"Advertiser","auth":{"type":"bearer","bearer":[{"key":"token","value":"{{advertiserToken}}","type":"string"}]},"item":[{"name":"Auth","item":[{"name":"Upload a registration file","request":{"method":"POST","description":"Before the account exists, the establishment form's files: `commercial_registration` (PDF, PNG or JPG, up to 10 MB) or `establishment_logo` (PNG or JPG, up to 5 MB). Send the returned `id` as `crFileId` / `logoFileId` with the form. Files not used within 24 hours are deleted. At most 20 uploads per hour from one network.","header":[{"key":"Accept-Language","value":"{{lang}}"}],"url":{"raw":"{{baseUrl}}/v1/advertiser/auth/register/files?purpose=establishment_logo","host":["{{baseUrl}}"],"path":["v1","advertiser","auth","register","files"],"query":[{"key":"purpose","value":"establishment_logo","disabled":false}]},"body":{"mode":"formdata","formdata":[{"key":"file","type":"file"}]},"auth":{"type":"noauth"}}},{"name":"Send an individual registration code","request":{"method":"POST","description":"Checks the individual form (step 1), then sends a 4-digit code to the mobile. Nothing is saved yet: send the same form again with the code to `register/individual`.","header":[{"key":"Accept-Language","value":"{{lang}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/v1/advertiser/auth/register/individual/otp","host":["{{baseUrl}}"],"path":["v1","advertiser","auth","register","individual","otp"]},"body":{"mode":"raw","raw":"{\n  \"name\": \"سارة أحمد\",\n  \"mobile\": \"0551234567\",\n  \"email\": \"sara@example.com\",\n  \"cityId\": \"string\",\n  \"acceptTerms\": true\n}","options":{"raw":{"language":"json"}}},"auth":{"type":"noauth"}}},{"name":"Register an individual with the code","request":{"method":"POST","description":"The individual form again, with the code: creates the account and starts a session. `nextStep` tells the app where to go.","header":[{"key":"Accept-Language","value":"{{lang}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/v1/advertiser/auth/register/individual","host":["{{baseUrl}}"],"path":["v1","advertiser","auth","register","individual"]},"body":{"mode":"raw","raw":"{\n  \"name\": \"سارة أحمد\",\n  \"mobile\": \"0551234567\",\n  \"email\": \"sara@example.com\",\n  \"cityId\": \"string\",\n  \"acceptTerms\": true,\n  \"code\": \"1234\",\n  \"deviceName\": \"iPhone 16\"\n}","options":{"raw":{"language":"json"}}},"auth":{"type":"noauth"}},"event":[{"listen":"test","script":{"type":"text/javascript","exec":["// Saves the new session token for the requests that follow.","if (pm.response.code >= 200 && pm.response.code < 300) {","  const token = pm.response.json()[\"accessToken\"];","  if (token) pm.collectionVariables.set(\"advertiserToken\", token);","}"]}}]},{"name":"Send an establishment registration code","request":{"method":"POST","description":"Checks the establishment form (step 1), including its uploaded files, then sends a 4-digit code to the manager mobile. Nothing is saved yet: send the same form again with the code to `register/establishment`.","header":[{"key":"Accept-Language","value":"{{lang}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/v1/advertiser/auth/register/establishment/otp","host":["{{baseUrl}}"],"path":["v1","advertiser","auth","register","establishment","otp"]},"body":{"mode":"raw","raw":"{\n  \"establishmentName\": \"مؤسسة المقصد للتجارة\",\n  \"crNumber\": \"1010123456\",\n  \"name\": \"خالد العتيبي\",\n  \"mobile\": \"0551234567\",\n  \"email\": \"info@company.sa\",\n  \"cityId\": \"string\",\n  \"taxNumber\": \"300000000000003\",\n  \"address\": \"طريق الملك فهد، حي العليا\",\n  \"crFileId\": \"string\",\n  \"logoFileId\": \"string\",\n  \"acceptTerms\": true\n}","options":{"raw":{"language":"json"}}},"auth":{"type":"noauth"}}},{"name":"Register an establishment with the code","request":{"method":"POST","description":"The establishment form again, with the code: creates the account, attaches its files and starts a session.","header":[{"key":"Accept-Language","value":"{{lang}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/v1/advertiser/auth/register/establishment","host":["{{baseUrl}}"],"path":["v1","advertiser","auth","register","establishment"]},"body":{"mode":"raw","raw":"{\n  \"establishmentName\": \"مؤسسة المقصد للتجارة\",\n  \"crNumber\": \"1010123456\",\n  \"name\": \"خالد العتيبي\",\n  \"mobile\": \"0551234567\",\n  \"email\": \"info@company.sa\",\n  \"cityId\": \"string\",\n  \"taxNumber\": \"300000000000003\",\n  \"address\": \"طريق الملك فهد، حي العليا\",\n  \"crFileId\": \"string\",\n  \"logoFileId\": \"string\",\n  \"acceptTerms\": true,\n  \"code\": \"1234\",\n  \"deviceName\": \"iPhone 16\"\n}","options":{"raw":{"language":"json"}}},"auth":{"type":"noauth"}},"event":[{"listen":"test","script":{"type":"text/javascript","exec":["// Saves the new session token for the requests that follow.","if (pm.response.code >= 200 && pm.response.code < 300) {","  const token = pm.response.json()[\"accessToken\"];","  if (token) pm.collectionVariables.set(\"advertiserToken\", token);","}"]}}]},{"name":"Send a login code","request":{"method":"POST","description":"Sends a 4-digit code to a registered, active advertiser number. `MOBILE_NOT_REGISTERED` means the app should offer account creation.","header":[{"key":"Accept-Language","value":"{{lang}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/v1/advertiser/auth/login/otp","host":["{{baseUrl}}"],"path":["v1","advertiser","auth","login","otp"]},"body":{"mode":"raw","raw":"{\n  \"mobile\": \"0551234567\"\n}","options":{"raw":{"language":"json"}}},"auth":{"type":"noauth"}}},{"name":"Log in with the code","request":{"method":"POST","description":"Checks the login code and starts a session.","header":[{"key":"Accept-Language","value":"{{lang}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/v1/advertiser/auth/login","host":["{{baseUrl}}"],"path":["v1","advertiser","auth","login"]},"body":{"mode":"raw","raw":"{\n  \"mobile\": \"0551234567\",\n  \"code\": \"1234\",\n  \"deviceName\": \"iPhone 16\"\n}","options":{"raw":{"language":"json"}}},"auth":{"type":"noauth"}},"event":[{"listen":"test","script":{"type":"text/javascript","exec":["// Saves the new session token for the requests that follow.","if (pm.response.code >= 200 && pm.response.code < 300) {","  const token = pm.response.json()[\"accessToken\"];","  if (token) pm.collectionVariables.set(\"advertiserToken\", token);","}"]}}]},{"name":"Log out","request":{"method":"POST","description":"Ends the session of the token sent; it stops working immediately. Other devices stay signed in.","header":[{"key":"Accept-Language","value":"{{lang}}"}],"url":{"raw":"{{baseUrl}}/v1/advertiser/auth/logout","host":["{{baseUrl}}"],"path":["v1","advertiser","auth","logout"]}}}]},{"name":"Account","item":[{"name":"My account","request":{"method":"GET","header":[{"key":"Accept-Language","value":"{{lang}}"}],"url":{"raw":"{{baseUrl}}/v1/advertiser/account","host":["{{baseUrl}}"],"path":["v1","advertiser","account"]}}},{"name":"Change my language","request":{"method":"PATCH","description":"The \"Change language\" screen. Requests without `Accept-Language` or `?lang=` are answered in it from the next request, and SMS messages use it.","header":[{"key":"Accept-Language","value":"{{lang}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/v1/advertiser/account","host":["{{baseUrl}}"],"path":["v1","advertiser","account"]},"body":{"mode":"raw","raw":"{\n  \"language\": \"en\"\n}","options":{"raw":{"language":"json"}}}}},{"name":"Delete my account","request":{"method":"DELETE","description":"Call after the app's confirmation dialog; there is no SMS code. Your details and files are removed, every device is signed out (this one included), and your number, email (and CR number) can register a new, empty account.","header":[{"key":"Accept-Language","value":"{{lang}}"}],"url":{"raw":"{{baseUrl}}/v1/advertiser/account","host":["{{baseUrl}}"],"path":["v1","advertiser","account"]}}},{"name":"Send a code to a new number","request":{"method":"POST","description":"Step 1 of changing the mobile number: the code goes to the new number. Call again to resend once `resendAfterSeconds` have passed.","header":[{"key":"Accept-Language","value":"{{lang}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/v1/advertiser/account/mobile/otp","host":["{{baseUrl}}"],"path":["v1","advertiser","account","mobile","otp"]},"body":{"mode":"raw","raw":"{\n  \"mobile\": \"0551234567\"\n}","options":{"raw":{"language":"json"}}}}},{"name":"Change my mobile number","request":{"method":"POST","description":"Step 2: checks the code sent to the new number and swaps it in; the old number no longer signs in. Every other device is signed out; this one stays signed in. A wrong code keeps the old number.","header":[{"key":"Accept-Language","value":"{{lang}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/v1/advertiser/account/mobile","host":["{{baseUrl}}"],"path":["v1","advertiser","account","mobile"]},"body":{"mode":"raw","raw":"{\n  \"mobile\": \"0551234567\",\n  \"code\": \"1234\"\n}","options":{"raw":{"language":"json"}}}}}]},{"name":"Cities","item":[{"name":"List cities","request":{"method":"GET","description":"The active cities, for city pickers (registration, listings, filters), each with its region, sorted by name in the request language. Public. Send the chosen `id`; an inactive or unknown city is refused with `CITY_NOT_AVAILABLE`.","header":[{"key":"Accept-Language","value":"{{lang}}"}],"url":{"raw":"{{baseUrl}}/v1/advertiser/cities","host":["{{baseUrl}}"],"path":["v1","advertiser","cities"]},"auth":{"type":"noauth"}}}]},{"name":"Files","item":[{"name":"Upload a file","request":{"method":"POST","description":"Send the file as multipart/form-data in the `file` field. Its type is checked from its bytes, not its name. Then pass the returned `id` to the endpoint that uses it; files unused after 24 hours are deleted.\n\n- `listing_image`: PNG, JPG, up to 5 MB, public\n- `listing_video`: MP4, MOV, up to 20 MB, public\n- `advertiser_avatar`: PNG, JPG, up to 5 MB, public\n- `establishment_logo`: PNG, JPG, up to 5 MB, public\n- `commercial_registration`: PDF, PNG, JPG, up to 10 MB, private\n- `chat_image`: PNG, JPG, WEBP, up to 5 MB, private\n- `chat_file`: PDF, up to 10 MB, private\n- `chat_voice`: M4A, AAC, up to 3 MB, private","header":[{"key":"Accept-Language","value":"{{lang}}"}],"url":{"raw":"{{baseUrl}}/v1/advertiser/files?purpose=listing_image","host":["{{baseUrl}}"],"path":["v1","advertiser","files"],"query":[{"key":"purpose","value":"listing_image","disabled":false}]},"body":{"mode":"formdata","formdata":[{"key":"file","type":"file"}]}}}]}]},{"name":"Admin","auth":{"type":"bearer","bearer":[{"key":"token","value":"{{adminToken}}","type":"string"}]},"item":[{"name":"Files","item":[{"name":"Upload a file","request":{"method":"POST","description":"Send the file as multipart/form-data in the `file` field. Its type is checked from its bytes, not its name. Then pass the returned `id` to the endpoint that uses it; files unused after 24 hours are deleted.\n\n- `establishment_logo`: PNG, JPG, up to 5 MB, public\n- `commercial_registration`: PDF, PNG, JPG, up to 10 MB, private\n- `banner_media`: PNG, JPG, WEBP, MP4, up to 10 MB, public\n- `category_icon`: PNG, JPG, WEBP, up to 2 MB, public\n- `package_icon`: PNG, JPG, WEBP, up to 2 MB, public\n- `admin_avatar`: PNG, JPG, WEBP, up to 5 MB, public","header":[{"key":"Accept-Language","value":"{{lang}}"}],"url":{"raw":"{{baseUrl}}/v1/admin/files?purpose=establishment_logo","host":["{{baseUrl}}"],"path":["v1","admin","files"],"query":[{"key":"purpose","value":"establishment_logo","disabled":false}]},"body":{"mode":"formdata","formdata":[{"key":"file","type":"file"}]}}}]}]},{"name":"System","auth":{"type":"noauth"},"item":[{"name":"Download a file\n\nPublic files (listing media, logos, avatars) need nothing else. Private\nfiles (commercial registrations, chat attachments) need the `expires` and `signature` from their URL; an invalid or\nexpired link answers 404. Supports `Range` for video seeking.","request":{"method":"GET","header":[{"key":"Accept-Language","value":"{{lang}}"}],"url":{"raw":"{{baseUrl}}/files/:id","host":["{{baseUrl}}"],"path":["files",":id"],"query":[{"key":"signature","value":"","disabled":true,"description":"Private files only."},{"key":"expires","value":"","disabled":true,"description":"Private files only."}],"variable":[{"key":"id","value":"string"}]}}},{"name":"Readiness: MySQL and Redis\n\n200 while the API can serve requests (`ok`, or `degraded` when only the\ncache is down), 503 when the database is unreachable. The Docker image's\nHEALTHCHECK calls this.","request":{"method":"GET","header":[{"key":"Accept-Language","value":"{{lang}}"}],"url":{"raw":"{{baseUrl}}/health","host":["{{baseUrl}}"],"path":["health"]}}},{"name":"Liveness: the process is up","request":{"method":"GET","header":[{"key":"Accept-Language","value":"{{lang}}"}],"url":{"raw":"{{baseUrl}}/health/live","host":["{{baseUrl}}"],"path":["health","live"]}}}]}]}