این راهنما بر اساس سند دستورالعمل فنی نحوه اتصال به سامانه مودیان (نسخه ۱۴۰۱۰۴۲۲) تهیه شده و شامل تمام جزئیات فنی مورد نیاز برای پیادهسازی اتصال به سامانه است.
- معماری کلی سامانه
- ساختار شناسه یکتای مالیاتی (taxid)
- فرآیند رمزنگاری و امضا
- ساختار JSON صورتحساب
- انواع صورتحساب
- فیلدهای سرصورتحساب (Header)
- فیلدهای اقلام (Body)
- ساختار بسته (Packet)
- API Endpoints
- کدهای خطا
- الگوریتم Verhoeff
سامانه مودیان از معماری RESTful API استفاده میکند. ارتباط به دو صورت انجام میشود:
- برای دریافت توکن، استعلام، دریافت لیست کالا
- پاسخ فوری دریافت میشود
- برای ارسال صورتحساب
- فقط شماره مرجع دریافت میشود
- استعلام وضعیت جداگانه
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Client │──────▶│ Gateway │──────▶│ Tax Core │
│ (Your App) │◀──────│ (tp.tax) │◀──────│ Server │
└──────────────┘ └──────────────┘ └──────────────┘
│ │ │
│ 1. GET_TOKEN │ │
│────────────────────▶ │
│◀──────────────────── │
│ │ │
│ 2. SEND_INVOICE │ │
│────────────────────▶ (Async) │
│◀──────────────────── │
│ referenceNumber │ │
│ │ │
│ 3. INQUIRY │ │
│────────────────────▶ (Sync) │
│◀──────────────────── │
│ Final Status │ │
شناسه یکتای مالیاتی یک رشته ۲۲ کاراکتری است که برای هر صورتحساب یکتا است.
taxid = fiscalId (6) + serial (10 hex) + date (6)
| جزء | طول | توضیح | مثال |
|---|
fiscalId | 6 | شناسه حافظه مالیاتی | A1B2C3 |
serial | 10 | شماره سریال (hex) | 0000000001 |
date | 6 | تاریخ شمسی (YYMMDD) | 040315 |
A1B2C3 + 0000000001 + 040315 = A1B2C30000000001040315
import moment from 'moment-jalaali';
function generateTaxId(
fiscalId: string, // 6 chars
serial: number // incrementing number
): string {
// Serial to 10-char hex
const serialHex = serial.toString(16).toUpperCase().padStart(10, '0');
// Jalali date YYMMDD
const now = moment();
const year = (now.jYear() % 100).toString().padStart(2, '0');
const month = (now.jMonth() + 1).toString().padStart(2, '0');
const day = now.jDate().toString().padStart(2, '0');
const dateStr = `${year}${month}${day}`;
return `${fiscalId}${serialHex}${dateStr}`;
}
// Example usage
const taxid = generateTaxId('A1B2C3', 1);
// Result: A1B2C30000000001040315
شکل ۱: نمودار آمادهسازی صورتحساب
┌─────────────────────────────────────────────────────────────┐
│ 1. ساخت JSON صورتحساب │
├─────────────────────────────────────────────────────────────┤
│ 2. نرمالسازی JSON (جدول ۱) │
├─────────────────────────────────────────────────────────────┤
│ 3. امضای دیجیتال با RSA-SHA256 │
├─────────────────────────────────────────────────────────────┤
│ 4. تولید کلید متقارن (AES-256) و IV (12 bytes) │
├─────────────────────────────────────────────────────────────┤
│ 5. رمزگذاری داده با AES-256-GCM │
├─────────────────────────────────────────────────────────────┤
│ 6. رمزگذاری کلید متقارن با کلید عمومی سرور (RSA-OAEP) │
├─────────────────────────────────────────────────────────────┤
│ 7. ساخت بسته نهایی │
└─────────────────────────────────────────────────────────────┘
| ردیف | قاعده | مثال |
|---|
| 1 | کلیدها به ترتیب الفبایی (ASCII) | {"a":1,"b":2} نه {"b":2,"a":1} |
| 2 | بدون فاصله اضافی | {"a":1} نه { "a" : 1 } |
| 3 | اعداد بدون صفر اضافی | 1.5 نه 1.50 |
| 4 | رشتهها با escape مناسب | "text\"quoted" |
| 5 | بدون BOM | UTF-8 without BOM |
function normalizeJson(data: any): string {
if (data === null) return 'null';
if (typeof data === 'boolean') return data ? 'true' : 'false';
if (typeof data === 'number') return data.toString();
if (typeof data === 'string') return JSON.stringify(data);
if (Array.isArray(data)) {
const items = data.map(item => normalizeJson(item));
return `[${items.join(',')}]`;
}
// Sort keys alphabetically
const sortedKeys = Object.keys(data).sort();
const parts = sortedKeys.map(key => {
const value = normalizeJson(data[key]);
return `"${key}":${value}`;
});
return `{${parts.join(',')}}`;
}
import * as crypto from 'crypto';
function signData(normalizedJson: string, privateKey: string): string {
const sign = crypto.createSign('RSA-SHA256');
sign.update(normalizedJson, 'utf8');
const signature = sign.sign(privateKey, 'base64');
return signature;
}
function encryptData(
data: string,
symmetricKey: Buffer, // 32 bytes
iv: Buffer // 12 bytes
): { encrypted: string; authTag: string } {
const cipher = crypto.createCipheriv('aes-256-gcm', symmetricKey, iv);
let encrypted = cipher.update(data, 'utf8', 'base64');
encrypted += cipher.final('base64');
const authTag = cipher.getAuthTag().toString('base64');
return { encrypted, authTag };
}
function encryptSymmetricKey(
symmetricKey: Buffer,
serverPublicKey: string
): string {
const encrypted = crypto.publicEncrypt(
{
key: serverPublicKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256',
},
symmetricKey
);
return encrypted.toString('base64');
}
{
"header": {
"taxid": "A1B2C30000000001040315",
"indatim": 1702800000000,
"indati2m": 1702800000000,
"inty": 1,
"inp": 1,
"ins": 1,
"tins": "14001234567",
"tob": 2,
"bid": "1234567890",
"tinb": "14009876543",
"sbc": "1234567890",
"bbc": "0987654321",
"bpc": "1234567890",
"ft": 1,
"bpn": "09121234567",
"scln": "خیابان ولیعصر",
"scc": "",
"crn": "",
"billid": "",
"tprdis": 1000000,
"tdis": 50000,
"tadis": 950000,
"tvam": 95000,
"todam": 0,
"tbill": 1045000,
"setm": 1,
"cap": 1045000,
"insp": 0,
"tvop": 0,
"tax17": 0,
"inno": "0000000001"
},
"body": [
{
"sstid": "2720000044801",
"sstt": "محصول نمونه",
"am": 2,
"mu": "164",
"fee": 500000,
"cfee": 0,
"cut": 0,
"exr": 0,
"prdis": 1000000,
"dis": 50000,
"adis": 950000,
"vra": 10,
"vam": 95000,
"odt": null,
"odr": 0,
"odam": 0,
"olt": null,
"olr": 0,
"olam": 0,
"consfee": 0,
"spro": 0,
"bros": 0,
"tcpbs": 0,
"cop": 0,
"vop": 0,
"bsrn": "",
"tsstam": 1045000
}
],
"payments": []
}
| کد | نوع | توضیح |
|---|
| 1 | نوع ۱ | خریدار حقیقی/حقوقی شناخته شده (B2B) |
| 2 | نوع ۲ | خریدار حقیقی ناشناس (B2C) |
| 3 | نوع ۳ | صادراتی |
| کد | الگو | کاربرد |
|---|
| 1 | فروش | صورتحساب عادی |
| 2 | برگشت از فروش | کالای برگشتی |
| 3 | ابطالی | ابطال کامل صورتحساب |
| 4 | اصلاحی | تصحیح جزئی |
| کد | موضوع |
|---|
| 1 | اصلی |
| 2 | الحاقی |
| 3 | ابطالی |
| 4 | اصلاحی |
| 5 | برگشت از فروش |
| فیلد | نوع | الزامی | توضیح |
|---|
taxid | string(22) | ✅ | شناسه یکتای مالیاتی |
indatim | number | ✅ | تاریخ و زمان صدور (Unix ms) |
indati2m | number | ✅ | تاریخ و زمان ایجاد (Unix ms) |
inty | number | ✅ | نوع صورتحساب (1-3) |
inno | string(10) | ✅ | شماره صورتحساب داخلی (hex) |
irtaxid | string(22) | ❌ | شناسه مرجع (برای ابطال/اصلاح) |
inp | number | ✅ | الگوی صورتحساب (1-4) |
ins | number | ✅ | موضوع صورتحساب (1-5) |
tins | string | ✅ | کد اقتصادی فروشنده |
tinb | string | ❌ | کد اقتصادی خریدار |
bid | string | ❌ | شناسه ملی خریدار |
sbc | string(10) | ✅ | کد پستی فروشنده |
bbc | string(10) | ❌ | کد پستی خریدار |
tprdis | number | ✅ | جمع مبلغ قبل از تخفیف |
tdis | number | ✅ | جمع تخفیفات |
tadis | number | ✅ | جمع مبلغ بعد از تخفیف |
tvam | number | ✅ | جمع مالیات بر ارزش افزوده |
todam | number | ✅ | جمع سایر مالیاتها |
tbill | number | ✅ | جمع کل صورتحساب |
setm | number | ✅ | روش تسویه (1=نقد, 2=نسیه, 3=ترکیب) |
cap | number | ❌ | مبلغ پرداختی نقدی |
insp | number | ❌ | مبلغ نسیه |
| فیلد | نوع | الزامی | توضیح |
|---|
sstid | string(13) | ✅ | شناسه کالا/خدمت |
sstt | string | ✅ | شرح کالا/خدمت |
am | number | ✅ | تعداد/مقدار |
mu | string | ✅ | واحد اندازهگیری |
fee | number | ✅ | مبلغ واحد (قبل از تخفیف) |
prdis | number | ✅ | مبلغ قبل از تخفیف (fee × am) |
dis | number | ✅ | تخفیف |
adis | number | ✅ | مبلغ بعد از تخفیف |
vra | number | ✅ | نرخ مالیات (%) |
vam | number | ✅ | مبلغ مالیات |
tsstam | number | ✅ | جمع کل قلم |
prdis = fee × am # مبلغ قبل از تخفیف
adis = prdis - dis # مبلغ بعد از تخفیف
vam = round(adis × vra / 100) # مالیات بر ارزش افزوده
tsstam = adis + vam + odam + olam # جمع کل قلم
# سرجمع:
tprdis = Σ(prdis) # جمع مبلغ قبل از تخفیف
tdis = Σ(dis) # جمع تخفیفات
tadis = tprdis - tdis # جمع مبلغ بعد از تخفیف
tvam = Σ(vam) # جمع مالیات
tbill = tadis + tvam + todam # جمع کل صورتحساب
{
"packets": [
{
"uid": "550e8400-e29b-41d4-a716-446655440000",
"packetType": "INVOICE.V01",
"retry": false,
"data": "BASE64_ENCRYPTED_DATA",
"encryptionKeyId": "server-key-id-123",
"symmetricKey": "BASE64_ENCRYPTED_SYMMETRIC_KEY",
"iv": "BASE64_IV_12_BYTES",
"fiscalId": "A1B2C3",
"dataSignature": "BASE64_SIGNATURE"
}
],
"signature": "BASE64_REQUEST_SIGNATURE"
}
{
"signature": "",
"signatureKeyId": "",
"timestamp": 1702800000000,
"result": {
"uid": "550e8400-e29b-41d4-a716-446655440000",
"referenceNumber": "REF-12345",
"errorCode": null,
"errorDetail": null
}
}
Production: https://tp.tax.gov.ir/req/api/self-tsp
Sandbox: https://sandboxrc.tax.gov.ir/req/api/self-tsp
| Endpoint | HTTP | توضیح |
|---|
/sync/SEND_INVOICE | POST | ارسال صورتحساب |
| Endpoint | HTTP | توضیح |
|---|
/sync/GET_TOKEN | POST | دریافت توکن |
/sync/INQUIRY_BY_UID/{uid} | GET | استعلام با uid |
/sync/INQUIRY_BY_REFERENCE_NUMBER/{ref} | GET | استعلام با شماره مرجع |
/sync/GET_FISCAL_INFORMATION | GET | اطلاعات حافظه مالیاتی |
/sync/GET_ECONOMIC_CODE_INFORMATION/{code} | GET | استعلام کد اقتصادی |
/sync/GET_SERVICE_STUFF_LIST | GET | لیست کالا/خدمات |
/sync/GET_SERVER_INFORMATION | GET | اطلاعات سرور |
| کد | پیام | راهحل |
|---|
| 00000 | موفق | - |
| 00001 | خطای داخلی سرور | صبر و تلاش مجدد |
| 00002 | درخواست نامعتبر | بررسی ساختار JSON |
| 00003 | احراز هویت ناموفق | توکن جدید دریافت کنید |
| 00004 | دسترسی غیرمجاز | بررسی مجوزها |
| 00005 | محدودیت نرخ | صبر و تلاش مجدد |
| 00006 | خطای رمزگذاری | بررسی کلیدها و الگوریتم |
| 00600 | خطای امضا | بررسی نرمالسازی JSON |
| کد | پیام |
|---|
| 0100501 | شناسه یکتای مالیاتی تکراری |
| 0100502 | فرمت شناسه یکتا نامعتبر |
| 0100503 | تاریخ صدور نامعتبر |
| 0100504 | کد اقتصادی فروشنده نامعتبر |
| 0100505 | کد پستی فروشنده نامعتبر |
| 0100506 | کد اقتصادی خریدار الزامی (نوع ۱) |
| 0100507 | نوع صورتحساب نامعتبر |
| کد | پیام |
|---|
| 0303301 | شناسه کالا با نرخ مالیات مطابقت ندارد |
| 0303302 | شناسه کالا یافت نشد |
| 0303303 | واحد اندازهگیری نامعتبر |
| کد | پیام | فرمول صحیح |
|---|
| 0401001 | مالیات قلم اشتباه | vam = adis × vra / 100 |
| 0501001 | تخفیف اشتباه | adis = prdis - dis |
| 0501002 | مبلغ قبل از تخفیف اشتباه | prdis = fee × am |
| 0501003 | جمع صورتحساب اشتباه | tbill = tadis + tvam + todam |
الگوریتم Verhoeff برای اعتبارسنجی کدهای اقتصادی و شناسه ملی استفاده میشود.
// جدول ضرب d
const d = [
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
[1, 2, 3, 4, 0, 6, 7, 8, 9, 5],
[2, 3, 4, 0, 1, 7, 8, 9, 5, 6],
[3, 4, 0, 1, 2, 8, 9, 5, 6, 7],
[4, 0, 1, 2, 3, 9, 5, 6, 7, 8],
[5, 9, 8, 7, 6, 0, 4, 3, 2, 1],
[6, 5, 9, 8, 7, 1, 0, 4, 3, 2],
[7, 6, 5, 9, 8, 2, 1, 0, 4, 3],
[8, 7, 6, 5, 9, 3, 2, 1, 0, 4],
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0],
];
// جدول جایگشت p
const p = [
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
[1, 5, 7, 6, 2, 8, 3, 0, 9, 4],
[5, 8, 0, 3, 7, 9, 6, 1, 4, 2],
[8, 9, 1, 6, 0, 4, 3, 5, 2, 7],
[9, 4, 5, 3, 1, 2, 6, 8, 7, 0],
[4, 2, 8, 6, 5, 7, 3, 9, 0, 1],
[2, 7, 9, 3, 8, 0, 6, 4, 1, 5],
[7, 0, 4, 6, 9, 1, 3, 2, 5, 8],
];
// جدول معکوس
const inv = [0, 4, 3, 2, 1, 5, 6, 7, 8, 9];
function validateVerhoeff(num: string): boolean {
let c = 0;
const numArray = num.split('').reverse().map(Number);
for (let i = 0; i < numArray.length; i++) {
c = d[c][p[i % 8][numArray[i]]];
}
return c === 0;
}
نرمافزار بارکد تمام این پیچیدگیها را برای شما انجام میدهد. کافیست گواهی دیجیتال خود را وارد کنید و فاکتورها به صورت خودکار ارسال میشوند.