پرش به محتوا
مستندات مهراد

ارتباط سازمانی (امضای درخواست)

سوپر اپلیکیشن مهراد

این سند نحوهٔ ارتباط سرور-به-سرور سازمان‌ها با مهراد را توضیح می‌دهد؛ ارتباطی که در آن هر request با کلید خصوصی سازمان امضا می‌شود. کاربرد آن، اعطای اعتبار به کاربران بر اساس کد ملی و دریافت آمار سفارش‌های همان کاربران است.


نمای کلی

بر خلاف سایر روش‌های این مستندات، در ارتباط سازمانی token صادر نمی‌شود. به جای آن، هر request جداگانه امضا می‌شود: سازمان رشته‌ای از مشخصات request می‌سازد، آن را با کلید خصوصی خود امضا می‌کند و امضا را در header ارسال می‌کند. مهراد امضا را با کلید عمومی سازمان بررسی می‌کند.

1

سازمانساخت body درخواست

2

سازمانساخت رشتهٔ امضا از METHOD، PATH، TIMESTAMP و body

3

سازمانهش SHA-256 و امضا با کلید خصوصی

4

سازمانارسال درخواست همراه با چهار header امنیتی

5

مهرادبررسی زمان، کلید، آی‌پی و امضا، سپس پردازش درخواست


پیش‌نیازها

موردتوضیح
x-api-keyکلید شناسایی اپلیکیشن سازمانی
زوج کلیدکلید خصوصی نزد سازمان می‌ماند؛ تنها کلید عمومی در مهراد نگهداری می‌شود
credit_type_idID نوع اعتبار؛ باید متعلق به همین سازمان باشد
لیست سفید آی‌پی (اختیاری)در صورت تنظیم، 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
بخشمقدار
METHODHTTP method با حروف بزرگ، مثلاً POST
PATHpath مربوط به request؛ اگر شامل query string باشد، باید همراه با آن به کار برود
TIMESTAMPزمان UTC به قالب ISO8601 و بدون میلی‌ثانیه، مثلاً 2026-09-19T12:00:00Z
PAYLOADدقیقاً همان body که ارسال می‌شود

چون امضا روی بایت‌های دقیق body محاسبه می‌شود، body باید یک‌بار ساخته شود و همان رشته هم امضا و هم ارسال شود. اگر پس از امضا حتی یک فاصله در body تغییر کند، امضا نامعتبر می‌شود. برای request بدون body، این بخش رشتهٔ خالی است.

گام ۲ — هش و امضا

  1. رشتهٔ امضا با SHA-256 هش و خروجی به صورت hex درآورده می‌شود.
  2. همان رشتهٔ hex با کلید خصوصی سازمان امضا می‌شود.
  3. امضای باینری به hex تبدیل و در header x-signature ارسال می‌شود.

توجه: آنچه امضا می‌شود، رشتهٔ hexِ هش است، نه بایت‌های خام هش و نه خود رشتهٔ امضا.

Python
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"]
Node.js
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-idID یکتای این 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)

POST/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
}

اعطای اعتبار

POST/gateway/transfer/credit

response موفق: 201 Created.

اعتبار بر اساس کد ملی ثبت می‌شود. اگر کاربری با این کد ملی احراز هویت شده باشد، اعتبار بلافاصله به او تعلق می‌گیرد؛ در غیر این صورت اعتبار نزد کد ملی نگه داشته می‌شود (user_id در response خالی است) و پس از احراز هویت کاربر در اختیار او قرار می‌گیرد. اعطای مجدد برای همان کد ملی و همان نوع اعتبار، سقف اعتبار کاربر را افزایش می‌دهد.

ورودی

پارامترهای ورودی3 فیلد
national_codestringالزامی
کد ملی کاربر؛ دقیقاً ۱۰ رقم و با رقم کنترل معتبر. مقدار ورودی پیش از پردازش نرمال‌سازی و اعتبارسنجی می‌شود.
credit_type_iduuidالزامی
ID نوع اعتبار؛ مقداری که در فرآیند راه‌اندازی در اختیار سازمان قرار گرفته است و باید متعلق به همین سازمان باشد.
amountintegerالزامی
مبلغ اعتبار به ریال؛ حداقل ۱.
{
  "national_code": "0012345678",
  "credit_type_id": "c1d2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
  "amount": 5000000
}

Response

فیلدهای پاسخ6 فیلد
credit_iduuid
ID اعتبار؛ برای پیگیری باید ذخیره شود.
user_iduuid | null
ID کاربری که اعتبار برای او ثبت شده است. تا زمانی که کاربری با این کد ملی احراز هویت نشده باشد، خالی است.
national_codestring
کد ملی نرمال‌شدهٔ کاربر.
amountinteger
مبلغی که در همین درخواست اعطا شده است (ریال).
total_amountinteger
سقف اعتبار کاربر پس از این اعطا؛ مجموع همهٔ اعطاهای این نوع اعتبار (ریال).
balanceinteger
مبلغ قابل مصرف کاربر پس از این اعطا (ریال).
نمونه پاسخ
{
  "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).

بنابراین در صورت بروز خطای شبکه یا نامشخص ماندن نتیجه:

  1. request نباید با همان مقدار دوباره ارسال شود.
  2. ابتدا وضعیت اعطای قبلی بررسی می‌شود.
  3. اگر اعطا انجام نشده باشد، request با یک x-external-request-id جدید دوباره ارسال می‌شود.

آمار سفارش‌ها

GET/gateway/emdad/order/statistics

response موفق: 200 OK.

سفارش‌ها محدود به کاربرانی است که در سازمانِ همین اپلیکیشن پروفایل دارند یا عضو گروه کاربران آن سازمان‌اند. اگر هیچ فیلتری فرستاده نشود، همهٔ سفارش‌های همین کاربران برمی‌گردد.

رشتهٔ امضا مثل بقیهٔ requestهای این سند ساخته می‌شود. body این request خالی است، پس بخش PAYLOAD رشتهٔ خالی است و query string، اگر باشد، بخشی از PATH است.

پارامترها

پارامترهای ورودی4 فیلد
datestring
یک روز به وقت تهران، قالب `Y-m-d`؛ مثلاً `2024-11-10`. فقط سفارش‌های ساخته‌شده در همان روز برمی‌گردند.
national_idsstring
کد ملی کاربران، جدا شده با کاما. هر مقدار باید کد ملی معتبر ۱۰ رقمی باشد.
order_statusstring
یکی از `successful`، `canceled`، `hovered` یا `sent`.
service_idstring
ID سرویس سفارش، به‌صورت UUID. فقط سفارش‌های همین سرویس برمی‌گردند.

Response

بدنهٔ پاسخ { "status", "data" } است. data یک آبجکت است که کلید هر عضو آن کد ملی کاربر است.

فیلدهای پاسخ3 فیلد
visitedboolean
اگر کاربر دست‌کم یک بار وارد حساب خود شده بود `true` و در غیر این صورت `false`.
add_to_cartboolean
همیشه `true`؛ فقط کاربرانی که دست‌کم یک سفارش دارند در response می‌آیند.
ordersarray of objects
سفارش‌های این کاربر، از قدیمی‌ترین به جدیدترین.

فیلدهای هر عضو orders:

فیلدهای پاسخ7 فیلد
order_idstring
کد رهگیری سفارش.
order_statusstring
اگر پرداخت سفارش انجام نشد یا ارزش سفارش به صفر رسید `canceled`، اگر پرداخت هنوز باز بود `hovered`، اگر سفارش ارسال شده و هنوز تحویل نشده بود `sent` و در غیر این صورت `successful`.
shipment_costinteger
جمع هزینهٔ ارسال و خدمات سفارش منهای تخفیف، به ریال؛ جمع آن با مبلغ اقلام برابر `price` است و اگر تخفیف بیشتر از این هزینه‌ها باشد، منفی می‌شود.
order_created_atstring
زمان ایجاد سفارش به وقت تهران، قالب `Y-m-d H:M:S`؛ مثلاً `2024-11-10 08:47:58`.
priceinteger
مبلغ باقی‌ماندهٔ سفارش، به ریال.
shipmentsarray of objects
مرسوله‌های سفارش.
paymentsarray of objects
پرداخت‌ها و برگشت‌های سفارش.

فیلدهای هر عضو shipments:

فیلدهای پاسخ2 فیلد
statusstring
اگر سفارش `canceled` بود `canceled`، اگر مرسوله تحویل شده بود `full_delivered_to_customer` و در غیر این صورت `processing`.
itemsarray of objects
اقلام همین مرسوله.

فیلدهای هر عضو items:

فیلدهای پاسخ3 فیلد
item_namestring
نام کالا.
activeboolean
اگر سفارش `canceled` بود `false` و در غیر این صورت `true`.
priceinteger
مبلغ این قلم، به ریال.

فیلدهای هر عضو payments:

فیلدهای پاسخ4 فیلد
statusstring
اگر ردیف برگشت وجه بود یا پرداخت انجام شد `accepted`، اگر پرداخت هنوز باز بود `new` و در غیر این صورت `rejected`.
typestring
اگر ردیف مبلغ برگشته به کاربر بود `user_received` و در غیر این صورت `user_paid`.
methodstring
اگر مبلغ از کیف پول بود `online` و در غیر این صورت `credit`.
amountinteger
مبلغ این ردیف، به ریال.
نمونه پاسخ
{
  "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_HEADERS401header x-api-key ارسال نشده است.
ORGANIZATION_APP_MISSING_SIGNATURE_HEADERS401header x-signature ارسال نشده است.
ORGANIZATION_APP_MISSING_TIMESTAMP_HEADERS400header x-timestamp ارسال نشده است.
ORGANIZATION_APP_MISSING_EXTERNAL_REQUEST_ID_HEADERS400header x-external-request-id ارسال نشده است.
ORGANIZATION_APP_INVALID_TIMESTAMP400قالب زمان نادرست است یا اختلاف آن با سرور بیش از ۵ دقیقه است.
ORGANIZATION_APP_INVALID_SIGNATURE400امضا با کلید عمومی ثبت‌شده تطابق ندارد؛ رشتهٔ امضا و بایت‌های body باید بررسی شوند.
ORGANIZATION_APP_IP_ADDRESS_NOT_ALLOWED403آی‌پی request در لیست سفید نیست.
ORGANIZATION_APP_NOT_ACTIVE400اپلیکیشن سازمانی غیرفعال است.
ORGANIZATION_APP_NOT_FOUND404کلید ارسالی به هیچ اپلیکیشن سازمانی تعلق ندارد.
نمونه پاسخ
{
  "success": false,
  "error": {
    "code": "ORGANIZATION_APP_INVALID_SIGNATURE",
    "message": "امضای درخواست نامعتبر می‌باشد.",
    "details": null
  }
}

خطاهای اعطای اعتبار

کد خطاHTTPتوضیح و اقدام پیشنهادی
VALIDATION_ERROR422فیلدی نامعتبر است؛ مثلاً کد ملی معتبر نیست یا amount کمتر از ۱ است. جزئیات هر فیلد در error.details آمده است.
CREDIT_GRANT_IN_PROCESS400این x-external-request-id قبلاً استفاده شده است؛ وضعیت اعطای قبلی باید بررسی شود و در صورت نیاز، request با ID جدید ارسال شود.
CREDIT_TYPE_NOT_FOUND404credit_type_id یافت نشد یا متعلق به این سازمان نیست.
INVALID_CREDIT_SOURCE_ACCOUNT400حساب مبدأ این نوع اعتبار فعال نیست؛ با پشتیبانی مهراد هماهنگ شود.
FINANCIAL_OPERATIONS_FROZEN503عملیات مالی موقتاً متوقف شده است؛ تلاش مجدد باید با فاصلهٔ زمانی انجام شود.
نمونه پاسخ
{
  "success": false,
  "error": {
    "code": "CREDIT_GRANT_IN_PROCESS",
    "message": "اعطای اعتبار دیگری با همین شناسه در سیستم موجود می‌باشد.",
    "details": null
  }
}

خطاهای آمار سفارش

کد خطاHTTPتوضیح و اقدام پیشنهادی
INVALID_ORDER_STATISTICS_PARAMS422national_ids کد ملی معتبر ندارد.
VALIDATION_ERROR422date قالب 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 و خواندن سفارش‌ها با کلید کد ملی
  • مدیریت خطاهای امضا، پنجرهٔ زمانی و اعطای تکراری
  • هماهنگ نگه داشتن ساعت سرور

در این صفحه