العملاء
كل فاتورة مبيعات ودفعة تُنشأ عن طريق الـ API تحدد عميلاً. والعميل إما فرد أو شركة، ونوعه يحدد نوع الفاتورة: الشركة تصدر لها فاتورة ضريبية تعتمدها هيئة الزكاة والضريبة والجمارك، والفرد تصدر له فاتورة مبسطة. التفاصيل في ابدأ من هنا.
| الـ Endpoint | الصلاحية |
|---|---|
POST /v1/customers | customers:write |
GET /v1/customers | customers:read |
GET /v1/customers/{id} | customers:read |
PATCH /v1/customers/{id} | customers:write |
إنشاء عميل فرد
العميل الفرد يحتاج type و phone. ورقم الجوال بصيغة E.164: علامة + ثم رمز الدولة ثم الرقم، مثل +966501234567.
curl https://api.kosoor.com.sa/v1/customers \
-H "Authorization: Bearer ksr_test_..." \
-H "Idempotency-Key: 6f1d2c3b-4a5e-4f60-9b7a-8c9d0e1f2a3b" \
-H "Content-Type: application/json" \
-d '{
"type": "person",
"firstName": "Sara",
"lastName": "Al-Harbi",
"phone": "+966501234567",
"email": "sara@example.com",
"referenceId": "crm-88412"
}'
الرد 201 Created:
{
"id": "0f8fad5b-d9cb-469f-a165-70867728950e",
"object": "customer",
"type": "person",
"referenceId": "crm-88412",
"firstName": "Sara",
"lastName": "Al-Harbi",
"phone": "+966501234567",
"email": "sara@example.com",
"isActive": true,
"branchId": null,
"company": null,
"address": {
"street": null,
"buildingNumber": null,
"postalCode": null,
"district": null,
"city": null,
"country": null
},
"metadata": {},
"standardInvoiceMissing": null,
"createdAt": "2026-11-07T09:30:12.512Z"
}
ويمكن للعميل الفرد أن يرسل أيضاً address.street و address.district و address.city و address.country. أما buildingNumber و postalCode والكائن company فللشركات فقط، وكذلك الاستغناء عن رقم الجوال.
إنشاء عميل من نوع شركة
الشركة تحتاج type و company.name و address.country. والدولة رمز من حرفين حسب ISO 3166، مثل SA. ولا تحتاج الشركة رقم جوال، ولا تقبل firstName ولا lastName، فاسمها في company.name.
curl https://api.kosoor.com.sa/v1/customers \
-H "Authorization: Bearer ksr_test_..." \
-H "Idempotency-Key: 2b8e4f9a-1c3d-4e5f-8a6b-7c8d9e0f1a2b" \
-H "Content-Type: application/json" \
-d '{
"type": "company",
"phone": "+966112345678",
"company": {
"name": "Al Noor Logistics",
"vatNumber": "310123456700003"
},
"address": {
"street": "Prince Sultan Road",
"buildingNumber": "7421",
"postalCode": "23521",
"district": "Al Rawdah",
"city": "Jeddah",
"country": "SA"
},
"referenceId": "crm-90017"
}'
الرد 201 Created:
{
"id": "3f1c2a6e-8b4d-4e2a-9c1f-5d7e8a9b0c12",
"object": "customer",
"type": "company",
"referenceId": "crm-90017",
"firstName": null,
"lastName": null,
"phone": "+966112345678",
"email": null,
"isActive": true,
"branchId": null,
"company": {
"name": "Al Noor Logistics",
"vatNumber": "310123456700003",
"idType": null,
"idValue": null,
"creditLimit": 0,
"creditPeriodDays": null
},
"address": {
"street": "Prince Sultan Road",
"buildingNumber": "7421",
"postalCode": "23521",
"district": "Al Rawdah",
"city": "Jeddah",
"country": "SA"
},
"metadata": {},
"standardInvoiceMissing": [],
"createdAt": "2026-11-07T09:31:05.208Z"
}
ما تحتاجه الفاتورة الضريبية
يمكن إنشاء الشركة باسمها ودولتها فقط، لكن الفاتورة الضريبية تحتاج أكثر من ذلك. ويعرض standardInvoiceMissing الحقول التي ما زالت ناقصة لهذه الشركة بمساراتها، ويكون فارغاً إذا لم ينقص شيء:
| المطلوب | متى |
|---|---|
company.vatNumber، أو company.idType مع company.idValue | دائماً |
address.street و address.city و address.country | دائماً |
address.buildingNumber و address.postalCode و address.district | إذا كانت الدولة SA |
والفاتورة لشركة ما زال ينقصها أي منها ترجع 422 customer_address_incomplete، والقائمة نفسها في missing. تحقق من standardInvoiceMissing قبل إصدار الفاتورة، وأكمل الناقص بطلب PATCH.
قواعد الشركة
| الحقل | القاعدة |
|---|---|
company.vatNumber | 15 رقماً تبدأ بـ 3 وتنتهي بـ 3 (400 invalid_vat_number)، ولا يكون لعميل آخر (409 vat_number_taken) |
company.idType و company.idValue | يُرسلان معاً، إما كلاهما بقيمة أو كلاهما null، وقيمة idValue من 1 إلى 50 حرفاً ورقماً |
address.buildingNumber | 4 أرقام إذا كانت الدولة SA، وحتى 20 حرفاً في غيرها |
address.postalCode | 5 أرقام إذا كانت الدولة SA، وحتى 20 حرفاً في غيرها |
company.creditLimit و company.creditPeriodDays | في الرد فقط، ويحددهما التاجر من لوحة التحكم. حد الائتمان 0 يعني بلا حد. |
أنواع الهوية
إذا لم يكن للشركة رقم ضريبي، أو إلى جانبه، يحدد idType نوع الرقم الموجود في idValue. وهذه رموز الهيئة لهوية المشتري:
idType | الرقم |
|---|---|
CRN | السجل التجاري |
MOM | ترخيص وزارة الشؤون البلدية والقروية |
MLS | ترخيص وزارة الموارد البشرية |
700 | الرقم الموحد 700 |
SAG | ترخيص الاستثمار (الهيئة العامة للاستثمار سابقاً، وزارة الاستثمار حالياً) |
NAT | الهوية الوطنية |
GCC | هوية خليجية |
IQA | الإقامة |
PAS | جواز السفر |
OTH | أي هوية أخرى |
الحقول المشتركة لكل عميل
| الحقل | القاعدة |
|---|---|
type | person أو company، مطلوب عند الإنشاء ولا يتغير أبداً |
phone | بصيغة E.164، ولا يكون لعميل آخر (409 phone_taken) |
firstName و lastName | اسم العميل الفرد، حتى 100 حرف لكل منهما |
email | بريد إلكتروني صحيح، حتى 256 حرفاً |
referenceId | معرّفك الخاص للعميل، حتى 100 حرف، ولا يكون لعميل آخر (409 reference_id_taken)، ويُحدَّد مرة واحدة فقط |
branchId | الفرع الذي يتبعه العميل، ويجب أن يكون فرعاً يملك مفتاحك صلاحية عليه (403 branch_not_allowed) |
isActive | قيمته true ما لم ترسل false، والعميل المعطّل ترجع فواتيره ودفعاته 422 customer_inactive |
metadata | حتى 20 مفتاحاً، طول كل مفتاح حتى 40 حرفاً، وكل قيمة حتى 500 حرف |
عندما تكون القيمة مستخدمة، يحمل الحقل existingId في الخطأ معرّف العميل الذي يستخدمها. استخدمه بدل أن تنشئ عميلاً ثانياً:
{
"error": {
"type": "invalid_request_error",
"code": "phone_taken",
"message": "رقم الجوال مسجّل لعميل آخر.",
"param": "phone",
"docUrl": "https://docs.kosoor.com.sa/ar/errors#phone_taken",
"existingId": "0f8fad5b-d9cb-469f-a165-70867728950e"
}
}
البحث عن عميل
يقبل GET /v1/customers فلاتر تطابق القيمة تماماً، وتُجمع معاً بـ AND:
| الـ Parameter | يطابق |
|---|---|
phone | رقم الجوال بصيغة E.164 |
vatNumber | company.vatNumber |
idValue | company.idValue |
referenceId | معرّفك الخاص |
اكتب علامة + في رقم الجوال بالشكل %2B، لأن + في الـ Query String تُقرأ مسافة.
curl "https://api.kosoor.com.sa/v1/customers?phone=%2B966501234567" \
-H "Authorization: Bearer ksr_test_..."
{
"data": [
{
"id": "0f8fad5b-d9cb-469f-a165-70867728950e",
"object": "customer",
"type": "person",
"referenceId": "crm-88412",
"firstName": "Sara",
"lastName": "Al-Harbi",
"phone": "+966501234567",
"email": "sara@example.com",
"isActive": true,
"branchId": null,
"company": null,
"address": {
"street": null,
"buildingNumber": null,
"postalCode": null,
"district": null,
"city": null,
"country": null
},
"metadata": {},
"standardInvoiceMissing": null,
"createdAt": "2026-11-07T09:30:12.512Z"
}
],
"hasMore": false
}
إذا كانت data فارغة فلا يوجد عميل مطابق. وبدون فلاتر تعرض القائمة كل العملاء صفحة بعد صفحة، الأحدث أولاً. التفاصيل في تقسيم النتائج.
تعديل عميل
يغيّر PATCH /v1/customers/{id} الحقول الموجودة في الـ Body فقط، وتبقى الحقول الأخرى على قيمها. وكذلك داخل company و address: لا يتغير إلا ما ترسله.
curl -X PATCH https://api.kosoor.com.sa/v1/customers/3f1c2a6e-8b4d-4e2a-9c1f-5d7e8a9b0c12 \
-H "Authorization: Bearer ksr_test_..." \
-H "Idempotency-Key: 8d7c6b5a-4f3e-4d2c-9b1a-0f9e8d7c6b5a" \
-H "Content-Type: application/json" \
-d '{ "email": "accounts@alnoor.example", "address": { "street": "King Abdulaziz Road" } }'
والرد هو العميل كاملاً، كما يرجعه GET.
| ما ترسله | النتيجة |
|---|---|
| حقل بقيمة | يأخذ الحقل القيمة الجديدة |
email أو phone بقيمة null، أو firstName أو lastName للعميل الفرد | يُمسح الحقل، ولا يمكن مسح رقم جوال العميل الفرد |
metadata | تستبدل القائمة كلها، و {} أو null تفرغها |
type | يجب أن يساوي النوع الحالي، فالعميل لا يتغير نوعه |
referenceId | يُقبل مرة واحدة فقط ما دام العميل بلا referenceId، وبعدها ترجع أي قيمة مختلفة 400 invalid_parameter |
company أو address بقيمة null | 400 invalid_parameter، فأرسل الحقول التي تريد تغييرها بدلاً من ذلك |
isActive بقيمة null | 400 invalid_parameter، فأرسل true أو false |
وإذا خالف الطلب أي قاعدة فلا يتغير شيء.