ارتباط سازمانی (امضای درخواست)
سوپر اپلیکیشن مهراد
این سند نحوهٔ ارتباط سرور-به-سرور سازمانها با مهراد را توضیح میدهد؛ ارتباطی که در آن هر request با کلید خصوصی سازمان امضا میشود. کاربرد آن، اعطای اعتبار به کاربران بر اساس کد ملی و دریافت آمار سفارشهای همان کاربران است.
نمای کلی
بر خلاف سایر روشهای این مستندات، در ارتباط سازمانی token صادر نمیشود. به جای آن، هر request جداگانه امضا میشود: سازمان رشتهای از مشخصات request میسازد، آن را با کلید خصوصی خود امضا میکند و امضا را در header ارسال میکند. مهراد امضا را با کلید عمومی سازمان بررسی میکند.
سازمانساخت body درخواست
سازمانساخت رشتهٔ امضا از METHOD، PATH، TIMESTAMP و body
سازمانهش SHA-256 و امضا با کلید خصوصی
سازمانارسال درخواست همراه با چهار header امنیتی
مهرادبررسی زمان، کلید، آیپی و امضا، سپس پردازش درخواست
پیشنیازها
| مورد | توضیح |
|---|---|
x-api-key | کلید شناسایی اپلیکیشن سازمانی |
| زوج کلید | کلید خصوصی نزد سازمان میماند؛ تنها کلید عمومی در مهراد نگهداری میشود |
credit_type_id | ID نوع اعتبار؛ باید متعلق به همین سازمان باشد |
| لیست سفید آیپی (اختیاری) | در صورت تنظیم، requestها فقط از همان آدرسها پذیرفته میشوند |
همهٔ این مقادیر در فرآیند راهاندازی در اختیار سازمان قرار میگیرد. کلید خصوصی در مهراد ذخیره نمیشود و قابل بازیابی نیست؛ در صورت مفقود شدن باید زوج کلید جدیدی جایگزین شود.
الگوریتمهای پشتیبانیشده
- RSA-PSS با کلید ۲۰۴۸ بیتی (هش SHA-256،
salt_lengthبیشینه) - Ed25519
فهرست endpointها
| endpoint | کاربرد | authentication |
|---|---|---|
POST /gateway/organization-app/test | بررسی صحت تنظیمات و امضا | headerهای امضا |
POST /gateway/transfer/credit | اعطای اعتبار به یک کاربر | headerهای امضا |
GET /gateway/emdad/order/statistics | آمار سفارشهای کاربران سازمان | headerهای امضا |
ساخت امضا
گام ۱ — رشتهٔ امضا
رشتهٔ امضا از اتصال چهار مقدار زیر با کاراکتر | ساخته میشود:
METHOD|PATH|TIMESTAMP|PAYLOAD| بخش | مقدار |
|---|---|
METHOD | HTTP method با حروف بزرگ، مثلاً POST |
PATH | path مربوط به request؛ اگر شامل query string باشد، باید همراه با آن به کار برود |
TIMESTAMP | زمان UTC به قالب ISO8601 و بدون میلیثانیه، مثلاً 2026-09-19T12:00:00Z |
PAYLOAD | دقیقاً همان body که ارسال میشود |
چون امضا روی بایتهای دقیق body محاسبه میشود، body باید یکبار ساخته شود و همان رشته هم امضا و هم ارسال شود. اگر پس از امضا حتی یک فاصله در body تغییر کند، امضا نامعتبر میشود. برای request بدون body، این بخش رشتهٔ خالی است.
گام ۲ — هش و امضا
- رشتهٔ امضا با SHA-256 هش و خروجی به صورت hex درآورده میشود.
- همان رشتهٔ hex با کلید خصوصی سازمان امضا میشود.
- امضای باینری به hex تبدیل و در header
x-signatureارسال میشود.
توجه: آنچه امضا میشود، رشتهٔ hexِ هش است، نه بایتهای خام هش و نه خود رشتهٔ امضا.
import hashlib
import json
import uuid
from datetime import datetime, timezone
import requests
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ed25519, padding, rsa
BASE_URL = "https://api.mehrad.app"
def sign(private_key, string_to_sign: str) -> str:
digest_hex = hashlib.sha256(string_to_sign.encode("utf-8")).hexdigest()
message = digest_hex.encode("utf-8")
if isinstance(private_key, rsa.RSAPrivateKey):
signature = private_key.sign(
message,
padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH),
hashes.SHA256(),
)
elif isinstance(private_key, ed25519.Ed25519PrivateKey):
signature = private_key.sign(message)
else:
raise TypeError("unsupported key type")
return signature.hex()
def grant_credit(private_key_pem: str, api_key: str, body: dict) -> dict:
private_key = serialization.load_pem_private_key(private_key_pem.encode("utf-8"), password=None)
path = "/gateway/transfer/credit"
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
# build the body once and send exactly these bytes
payload = json.dumps(body, separators=(",", ":"), ensure_ascii=False)
string_to_sign = f"POST|{path}|{timestamp}|{payload}"
headers = {
"Content-Type": "application/json",
"x-api-key": api_key,
"x-timestamp": timestamp,
"x-signature": sign(private_key, string_to_sign),
"x-external-request-id": str(uuid.uuid4()),
}
response = requests.post(f"{BASE_URL}{path}", headers=headers, data=payload.encode("utf-8"), timeout=15)
response.raise_for_status()
return response.json()["data"]const crypto = require("crypto");
const BASE_URL = "https://api.mehrad.app";
function sign(privateKeyPem, stringToSign) {
const digestHex = crypto.createHash("sha256").update(stringToSign, "utf8").digest("hex");
const key = crypto.createPrivateKey(privateKeyPem);
// Ed25519 keys are signed in one shot with a null algorithm
if (key.asymmetricKeyType === "ed25519") {
return crypto.sign(null, Buffer.from(digestHex, "utf8"), key).toString("hex");
}
return crypto
.sign("sha256", Buffer.from(digestHex, "utf8"), {
key,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
saltLength: crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN,
})
.toString("hex");
}
async function grantCredit(privateKeyPem, apiKey, body) {
const path = "/gateway/transfer/credit";
const timestamp = new Date().toISOString().split(".")[0] + "Z";
// build the body once and send exactly this string
const payload = JSON.stringify(body);
const stringToSign = `POST|${path}|${timestamp}|${payload}`;
const res = await fetch(`${BASE_URL}${path}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": apiKey,
"x-timestamp": timestamp,
"x-signature": sign(privateKeyPem, stringToSign),
"x-external-request-id": crypto.randomUUID(),
},
body: payload,
});
if (!res.ok) throw new Error(`request failed: ${res.status}`);
return (await res.json()).data;
}گام ۳ — headerها
هر request باید این چهار header را داشته باشد:
| header | مقدار |
|---|---|
x-api-key | کلید اپلیکیشن سازمانی |
x-timestamp | همان زمانی که در رشتهٔ امضا به کار رفته است |
x-signature | امضا به صورت hex |
x-external-request-id | ID یکتای این request در سیستم سازمان |
POST /gateway/transfer/credit HTTP/1.1
Host: api.mehrad.app
Content-Type: application/json
x-api-key: YOUR_API_KEY
x-timestamp: 2026-09-19T12:00:00Z
x-external-request-id: 4b446a5e-ad25-47d4-b5b8-9d3d403ea219
x-signature: 3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b
{"national_code":"0012345678","credit_type_id":"c1d2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f","amount":5000000}پنجرهٔ زمانی
اختلاف x-timestamp با زمان سرور نباید بیشتر از ۵ دقیقه باشد. ساعت سرور باید با NTP هماهنگ نگه داشته شود و برای هر request، زمان تازه تولید شود.
بررسی تنظیمات (Self-check)
/gateway/organization-app/testاین endpoint پیش از پیادهسازی عملیات اصلی، برای اطمینان از درستی تنظیمات کلید، ساعت، امضا، وضعیت اپلیکیشن و آیپی به کار میرود. response موفق نشاندهندهٔ صحت عملکرد زنجیرهٔ امضاست.
این request به body نیاز ندارد؛ در این حالت بخش PAYLOAD در رشتهٔ امضا خالی میماند:
POST|/gateway/organization-app/test|2026-09-19T12:00:00Z|response موفق شامل وضعیت authentication و مشخصات اپلیکیشن سازمانی است:
{
"success": true,
"data": {
"status": "authenticated",
"app_id": "0b6f1e2d-3c4b-4a59-8e7f-6d5c4b3a2190",
"api_key": "YOUR_API_KEY",
"organization_id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c10",
"is_active": true,
"message": "Organization app authentication successful"
},
"meta": null
}اعطای اعتبار
/gateway/transfer/creditresponse موفق: 201 Created.
اعتبار بر اساس کد ملی ثبت میشود. اگر کاربری با این کد ملی احراز هویت شده باشد، اعتبار بلافاصله به او تعلق میگیرد؛ در غیر این صورت اعتبار نزد کد ملی نگه داشته میشود (user_id در response خالی است) و پس از احراز هویت کاربر در اختیار او قرار میگیرد. اعطای مجدد برای همان کد ملی و همان نوع اعتبار، سقف اعتبار کاربر را افزایش میدهد.
ورودی
national_ codestringالزامیcredit_ type_ iduuidالزامیamountintegerالزامی{
"national_code": "0012345678",
"credit_type_id": "c1d2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
"amount": 5000000
}Response
credit_ iduuiduser_ iduuid | nullnational_ codestringamountintegertotal_ amountintegerbalanceinteger{
"success": true,
"data": {
"credit_id": "7d9e0f1a-2b3c-4d5e-8f90-1a2b3c4d5e6f",
"user_id": "3f1a9c22-58b4-4f6e-9d77-1b2c3d4e5f60",
"national_code": "0012345678",
"amount": 5000000,
"total_amount": 5000000,
"balance": 5000000
},
"meta": null
}x-external-request-id و جلوگیری از اعطای تکراری
مقدار این header همراه اعتبار اعطاشده ثبت میشود و باید برای هر اعطا یکتا باشد.
اگر request با مقداری ارسال شود که قبلاً استفاده شده است، خطا برگردانده میشود و نتیجهٔ اعطای قبلی دوباره بازگردانده نمیشود. این سازوکار برای جلوگیری از اعطای تکراری است، نه تکرار response قبلی (idempotency).
بنابراین در صورت بروز خطای شبکه یا نامشخص ماندن نتیجه:
- request نباید با همان مقدار دوباره ارسال شود.
- ابتدا وضعیت اعطای قبلی بررسی میشود.
- اگر اعطا انجام نشده باشد، request با یک
x-external-request-idجدید دوباره ارسال میشود.
آمار سفارشها
/gateway/emdad/order/statisticsresponse موفق: 200 OK.
سفارشها محدود به کاربرانی است که در سازمانِ همین اپلیکیشن پروفایل دارند یا عضو گروه کاربران آن سازماناند. اگر هیچ فیلتری فرستاده نشود، همهٔ سفارشهای همین کاربران برمیگردد.
رشتهٔ امضا مثل بقیهٔ requestهای این سند ساخته میشود. body این request خالی است، پس بخش PAYLOAD رشتهٔ خالی است و query string، اگر باشد، بخشی از PATH است.
پارامترها
datestringnational_ idsstringorder_ statusstringservice_ idstringResponse
بدنهٔ پاسخ { "status", "data" } است. data یک آبجکت است که کلید هر عضو آن کد ملی کاربر است.
visitedbooleanadd_ to_ cartbooleanordersarray of objectsفیلدهای هر عضو orders:
order_ idstringorder_ statusstringshipment_ costintegerorder_ created_ atstringpriceintegershipmentsarray of objectspaymentsarray of objectsفیلدهای هر عضو shipments:
statusstringitemsarray of objectsفیلدهای هر عضو items:
item_ namestringactivebooleanpriceintegerفیلدهای هر عضو payments:
statusstringtypestringmethodstringamountinteger{
"status": 200,
"data": {
"0012345678": {
"visited": true,
"add_to_cart": true,
"orders": [
{
"order_id": "268714445",
"order_status": "sent",
"shipment_cost": 0,
"order_created_at": "2024-11-10 08:47:58",
"price": 380124600,
"shipments": [
{
"status": "processing",
"items": [
{
"item_name": "تلویزیون سام مدل 43C5300",
"active": true,
"price": 380124600
}
]
}
],
"payments": [
{
"status": "accepted",
"type": "user_paid",
"method": "credit",
"amount": 380000000
},
{
"status": "accepted",
"type": "user_paid",
"method": "online",
"amount": 124600
}
]
}
]
}
}
}فهرست خطاها
اگر request با خطا مواجه شود، response آن کدی غیر از ۲xx دارد و success در آن false است. کد خطا در فیلد error.code و متن قابل نمایش در error.message برگردانده میشود. ساختار کامل خطا در بخش مقدمه آمده است.
خطاهای authentication و امضا
این خطاها در همهٔ endpointهای این مستند، از جمله POST /gateway/organization-app/test، برگردانده میشوند.
| کد خطا | HTTP | توضیح و اقدام پیشنهادی |
|---|---|---|
| ORGANIZATION_APP_MISSING_API_KEY_HEADERS | 401 | header x-api-key ارسال نشده است. |
| ORGANIZATION_APP_MISSING_SIGNATURE_HEADERS | 401 | header x-signature ارسال نشده است. |
| ORGANIZATION_APP_MISSING_TIMESTAMP_HEADERS | 400 | header x-timestamp ارسال نشده است. |
| ORGANIZATION_APP_MISSING_EXTERNAL_REQUEST_ID_HEADERS | 400 | header x-external-request-id ارسال نشده است. |
| ORGANIZATION_APP_INVALID_TIMESTAMP | 400 | قالب زمان نادرست است یا اختلاف آن با سرور بیش از ۵ دقیقه است. |
| ORGANIZATION_APP_INVALID_SIGNATURE | 400 | امضا با کلید عمومی ثبتشده تطابق ندارد؛ رشتهٔ امضا و بایتهای body باید بررسی شوند. |
| ORGANIZATION_APP_IP_ADDRESS_NOT_ALLOWED | 403 | آیپی request در لیست سفید نیست. |
| ORGANIZATION_APP_NOT_ACTIVE | 400 | اپلیکیشن سازمانی غیرفعال است. |
| ORGANIZATION_APP_NOT_FOUND | 404 | کلید ارسالی به هیچ اپلیکیشن سازمانی تعلق ندارد. |
{
"success": false,
"error": {
"code": "ORGANIZATION_APP_INVALID_SIGNATURE",
"message": "امضای درخواست نامعتبر میباشد.",
"details": null
}
}خطاهای اعطای اعتبار
| کد خطا | HTTP | توضیح و اقدام پیشنهادی |
|---|---|---|
| VALIDATION_ERROR | 422 | فیلدی نامعتبر است؛ مثلاً کد ملی معتبر نیست یا amount کمتر از ۱ است. جزئیات هر فیلد در error.details آمده است. |
| CREDIT_GRANT_IN_PROCESS | 400 | این x-external-request-id قبلاً استفاده شده است؛ وضعیت اعطای قبلی باید بررسی شود و در صورت نیاز، request با ID جدید ارسال شود. |
| CREDIT_TYPE_NOT_FOUND | 404 | credit_type_id یافت نشد یا متعلق به این سازمان نیست. |
| INVALID_CREDIT_SOURCE_ACCOUNT | 400 | حساب مبدأ این نوع اعتبار فعال نیست؛ با پشتیبانی مهراد هماهنگ شود. |
| FINANCIAL_OPERATIONS_FROZEN | 503 | عملیات مالی موقتاً متوقف شده است؛ تلاش مجدد باید با فاصلهٔ زمانی انجام شود. |
{
"success": false,
"error": {
"code": "CREDIT_GRANT_IN_PROCESS",
"message": "اعطای اعتبار دیگری با همین شناسه در سیستم موجود میباشد.",
"details": null
}
}خطاهای آمار سفارش
| کد خطا | HTTP | توضیح و اقدام پیشنهادی |
|---|---|---|
| INVALID_ORDER_STATISTICS_PARAMS | 422 | national_ids کد ملی معتبر ندارد. |
| VALIDATION_ERROR | 422 | date قالب Y-m-d ندارد، order_status یکی از مقدارهای مجاز نیست، یا service_id قالب UUID ندارد. |
{
"success": false,
"error": {
"code": "INVALID_ORDER_STATISTICS_PARAMS",
"message": "کد ملی ارسالشده برای آمار سفارشها معتبر نیست.",
"details": null
}
}نکات امنیتی و عملیاتی
- کلید خصوصی باید فقط روی سرور و در محیط امن نگهداری شود و نباید در مخزن کد یا سمت کلاینت قرار گیرد.
- ساعت سرور باید با NTP هماهنگ نگه داشته شود تا پنجرهٔ ۵ دقیقهای نقض نشود.
- body امضاشده و body ارسالی باید دقیقاً یکی باشند؛ سریالسازی نباید دو بار انجام شود.
- برای هر اعطا باید یک
x-external-request-idتازه تولید و کنار رکورد مربوط در سیستم سازمان ذخیره شود. - برای تست،
https://dev-api.mehrad.appو اعتبارنامههای همان محیط استفاده میشود.
چکلیست پیادهسازی
- دریافت
x-api-key، ثبت کلید عمومی و نگهداری امن کلید خصوصی - دریافت
credit_type_id - پیادهسازی ساخت رشتهٔ امضا به قالب
METHOD|PATH|TIMESTAMP|PAYLOAD - هش SHA-256، امضای رشتهٔ hex و ارسال امضا به صورت hex
- ارسال هر چهار header امنیتی در همهٔ requestها
- بررسی تنظیمات با
POST /gateway/organization-app/test - تولید
x-external-request-idیکتا برای هر اعطا و ذخیرهٔ آن - پیادهسازی
POST /gateway/transfer/creditو ذخیرهٔcredit_id - پیادهسازی
GET /gateway/emdad/order/statisticsو خواندن سفارشها با کلید کد ملی - مدیریت خطاهای امضا، پنجرهٔ زمانی و اعطای تکراری
- هماهنگ نگه داشتن ساعت سرور