مستندات API درگاه کارت به کارت تینتپی (برای سایتهای اختصاصی)
این راهنما برای سایتهایی است که با PHP، Node.js، Python یا هر زبان دیگری نوشته شدهاند و از افزونهٔ ووکامرس استفاده نمیکنند. خلاصه: با API یک پرداخت میسازید، مشتری را به paymentUrl میفرستید، مشتری مبلغ دقیق را به کارت خودتان واریز میکند و پس از تأیید خودکار (با پیامک بانک روی اپ اندروید شما) وبهوک دریافت میکنید و با verify نتیجه را قطعی میکنید.
۱. پیشنیازها و دریافت کلید API
- در تینتپی ثبتنام کنید و احراز هویت (KYC) را کامل کنید.
- احراز هویت را کامل کنید و به بخش کارتبهکارت پنل بروید، کارت مقصد را ثبت کنید تا تأیید شود و اپ اندروید را با کد جفتسازی وصل کنید.
- کیف کارمزد کارتبهکارت را شارژ کنید. کارمزد فقط برای پرداختهای تأییدشده کسر میشود؛ مقدار فعلی:
GET https://tnetpay.com/api/c2c/public/pricing. - در پنل، بخش «کلیدهای API» یک کلید بسازید و فقط سمت سرور نگه دارید. کلید را در هدر
X-API-Key(یاAuthorization: Bearer ...) بفرستید.
آدرس پایه: https://tnetpay.com/api/v1/c2c
۲. ساخت پرداخت
POST /payments — هدر Idempotency-Key الزامی است (حداکثر ۱۲۸ نویسهٔ ASCII بدون فاصله)؛ نبودن آن خطای ۴۰۰ IDEMPOTENCY_KEY_REQUIRED میدهد.
| فیلد | نوع | توضیح |
|---|---|---|
| amount | عدد صحیح (تومان) | مبلغ سفارش به تومان؛ باید بزرگتر از صفر باشد. الزامی. |
| orderId | رشته | شناسهٔ سفارش در سایت شما. الزامی. |
| callbackUrl | URL | آدرس وبهوک شما (POST). الزامی. |
| returnUrl | URL | بازگشت مشتری پس از پرداخت. اختیاری. |
| description | رشته | توضیح سفارش. اختیاری. |
| payerMobile | رشته | موبایل پرداختکننده. اختیاری. |
| metadata | شیء JSON | دادههای دلخواه شما. اختیاری. |
curl -X POST 'https://tnetpay.com/api/v1/c2c/payments' \
-H 'Content-Type: application/json' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Idempotency-Key: order-1001' \
-d '{
"amount": 250000,
"orderId": "1001",
"callbackUrl": "https://shop.example/tnetpay/webhook",
"returnUrl": "https://shop.example/order/1001",
"description": "Order #1001"
}'نمونهٔ پاسخ (مبالغ رشتهای هستند):
{
"id": "c2c_01M68MJSCW2YKGWSFCD89MJS1T",
"status": "pending",
"amount": "250000",
"payAmount": "250037",
"payAmountRial": "2500370",
"uniqueSuffix": "37",
"fee": "...",
"card": { "bank": "...", "bankName": "...", "holderName": "...", "number": "6037 .... .... 1234", "last4": "1234" },
"paymentUrl": "https://tnetpay.com/pay/c2c/c2c_01M68MJSCW2YKGWSFCD89MJS1T",
"expiresAt": "2026-10-31T09:47:49.000Z",
"mode": "live"
}payAmount مبلغ دقیقی است که مشتری باید واریز کند: مبلغ سفارش بهاضافهٔ یک عدد کوچک یکتا (uniqueSuffix) تا هر واریز فقط با یک سفارش جور شود. payAmountRial همان مبلغ به ریال است.
۳. هدایت مشتری به صفحهٔ پرداخت
شناسهٔ id را روی سفارش ذخیره کنید و مشتری را به paymentUrl هدایت کنید. صفحهٔ پرداخت شمارهٔ کارت، نام صاحب کارت، مبلغ دقیق و زمان باقیمانده را نشان میدهد و وضعیت را خودکار بهروز میکند. پس از تأیید، اگر returnUrl داده باشید، مشتری با پارامترهای ?id=...&status=paid برمیگردد. این پارامترها فقط برای نمایش است؛ تحویل سفارش را فقط بر اساس verify انجام دهید.
۴. بررسی وضعیت
برای سرور خودتان: GET /payments/{id} (جزئیات کامل) یا POST /payments/{id}/verify (پاسخ کوتاه و قطعی). وضعیتها: pending، paid، manual_review، expired، cancelled، rejected.
curl -X POST 'https://tnetpay.com/api/v1/c2c/payments/c2c_01M68MJSCW2YKGWSFCD89MJS1T/verify' \
-H 'X-API-Key: YOUR_API_KEY'{
"status": "paid",
"paid": true,
"amount": "250000",
"payAmount": "250037",
"paidAt": "2026-10-31T09:30:12.000Z",
"orderId": "1001"
}برای صفحهٔ سمت مشتری (بدون کلید API) یک وضعیت عمومی و حداقلی هم هست:
curl 'https://tnetpay.com/api/pay/c2c/c2c_01M68MJSCW2YKGWSFCD89MJS1T/status'
# {"id":"c2c_...","status":"pending","final":false,"expiresAt":"...","paidAt":null,"serverTime":"..."}۵. وبهوک و تأیید آن
وقتی وضعیت پرداخت تغییر کند، تینتپی یک درخواست POST با بدنهٔ JSON به callbackUrl میفرستد. رویدادها: c2c.payment.paid، c2c.payment.cancelled، c2c.payment.rejected. هدرها: X-TNetPay-Event، X-TNetPay-Delivery، X-TNetPay-Attempt و امضای X-TNetPay-Signature/X-TNetPay-Timestamp.
{
"event": "c2c.payment.paid",
"id": "c2c_01M68MJSCW2YKGWSFCD89MJS1T",
"status": "paid",
"orderId": "1001",
"amount": "250000",
"payAmount": "250037",
"payAmountRial": "2500370",
"fee": "...",
"paidAt": "2026-10-31T09:30:12.000Z",
"timestamp": "2026-10-31T09:30:13.000Z"
}روش تأیید: کلید امضای وبهوک در اختیار پذیرنده قرار نمیگیرد، پس به بدنهٔ وبهوک اعتماد نکنید. با دریافت هر وبهوک، POST /payments/{id}/verify را با کلید API خودتان صدا بزنید و فقط اگر paid برابر true بود و orderId و مبلغ با سفارش شما یکی بود، سفارش را پرداختشده کنید.
با کد 2xx پاسخ دهید. اگر پاسخ شما خطا باشد یا در دسترس نباشید، ارسال دوباره تقریباً پس از ۱ دقیقه، ۵ دقیقه، ۳۰ دقیقه، ۲ ساعت، ۶ ساعت و ۲۴ ساعت تکرار میشود؛ پس پردازش وبهوک را تکرارپذیر بنویسید (یک سفارش فقط یک بار پرداختشده شود).
۶. Idempotency، انقضا و لغو
- Idempotency-Key: برای هر سفارش یک کلید ثابت (مثلاً
order-1001) بفرستید. تکرار همان درخواست همان پرداخت قبلی را برمیگرداند؛ تکرار با بدنهٔ متفاوت خطایIDEMPOTENCY_CONFLICTمیدهد. - انقضا: هر پرداخت
expiresAtدارد (مدت آن درpayment.ttlMinutesپاسخ pricing آمده است). پس از آن، صفحهٔ پرداخت پایانیافته نشان داده میشود. واریزی که کمی دیرتر برسد ممکن است تا مدتpayment.graceMinutesهمچنان تطبیق داده شود؛ پس سفارش را فقط باverifyقطعی کنید و پس از این مدت در صورت نیاز سفارش جدید بسازید. - لغو:
POST /payments/{id}/cancelفقط برای پرداختهایpending. - واریز با مبلغ اشتباه: خودکار تأیید نمیشود و در پنل برای بررسی دستی نمایش داده میشود.
۷. خطاها
خطاها معمولاً به شکل {"error":{"code":"...","message_fa":"...","message_en":"..."}} برمیگردند (خطاهای احراز کلید به شکل استاندارد statusCode/message).
| HTTP | کد | معنی و راهحل |
|---|---|---|
| 401 | API Key is required / Invalid API Key | هدر X-API-Key (یا Authorization: Bearer) نیست یا کلید نامعتبر است. |
| 403 | KYC_REQUIRED | احراز هویت پذیرنده تأیید نشده یا حساب غیرفعال است. |
| 403 | C2C_DISABLED | سرویس کارتبهکارت برای این حساب فعال نیست (احراز هویت را کامل کنید یا با پشتیبانی تماس بگیرید). |
| 400 | INVALID_AMOUNT | مبلغ صفر/منفی است، یا (در لغو) پرداخت دیگر در انتظار نیست. |
| 400 | IDEMPOTENCY_KEY_REQUIRED | هدر Idempotency-Key نیامده یا نامعتبر است (حداکثر ۱۲۸ نویسهٔ ASCII بدون فاصله). |
| 400 | NO_ACTIVE_CARD | هیچ کارت فعال و تأییدشدهای برای دریافت واریز ندارید. |
| 402 | C2C_SUSPENDED_LOW_BALANCE | (در فیلد error پاسخ) موجودی کیف کارمزد برای این پرداخت و پرداختهای باز دیگر کافی نیست؛ کیف را شارژ کنید. |
| 400 | AMOUNT_OUT_OF_RANGE | در این لحظه مبلغ یکتای آزاد برای این مبلغ پیدا نشد (پرداختهای باز زیادی با همین مبلغ)؛ کمی بعد دوباره تلاش کنید. |
| 409 | IDEMPOTENCY_CONFLICT | همان Idempotency-Key قبلاً با بدنهٔ متفاوت ارسال شده است. |
| 404 | NOT_FOUND | شناسهٔ پرداخت پیدا نشد یا متعلق به حساب شما نیست. |
| 429 | — | تعداد درخواست زیاد است (ایجاد پرداخت: حداکثر ۶۰ در دقیقه برای هر کلید)؛ کمی بعد دوباره تلاش کنید. |
۸. آزمایش
در حال حاضر کلید سندباکس عمومی صادر نمیشود؛ برای امتحان یک سفارش با مبلغ کم بسازید و از یک کارت دیگر واریز کنید. چون کارمزد فقط برای پرداخت تأییدشده کسر میشود، سفارشهای پرداختنشده هزینهای ندارند.
۹. نمونهکد کامل
PHP (بدون فریمورک)
<?php
// 1) Create a payment and redirect the buyer
const TNETPAY_API = 'https://tnetpay.com/api/v1/c2c';
$apiKey = getenv('TNETPAY_API_KEY');
function tnetpay_request(string $method, string $path, ?array $body = null, array $extraHeaders = []): array {
global $apiKey;
$ch = curl_init(TNETPAY_API . $path);
$headers = array_merge(['X-API-Key: ' . $apiKey, 'Accept: application/json'], $extraHeaders);
$opts = [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 20];
if ($body !== null) {
$headers[] = 'Content-Type: application/json';
$opts[CURLOPT_POSTFIELDS] = json_encode($body);
}
$opts[CURLOPT_HTTPHEADER] = $headers;
curl_setopt_array($ch, $opts);
$raw = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = json_decode((string) $raw, true) ?: [];
if ($code >= 400 || isset($data['error'])) {
throw new RuntimeException('TNetPay error: ' . ($data['error']['code'] ?? $code));
}
return $data;
}
$orderId = '1001';
$payment = tnetpay_request('POST', '/payments', [
'amount' => 250000, // toman, integer
'orderId' => $orderId,
'callbackUrl' => 'https://shop.example/tnetpay/webhook.php',
'returnUrl' => 'https://shop.example/order.php?id=' . $orderId,
], ['Idempotency-Key: order-' . $orderId]);
// save $payment['id'] on your order, then:
header('Location: ' . $payment['paymentUrl']);
exit;
// 2) webhook.php — never trust the body alone; re-check with verify
$event = json_decode(file_get_contents('php://input'), true);
$check = tnetpay_request('POST', '/payments/' . rawurlencode($event['id']) . '/verify');
if ($check['paid'] === true && $check['orderId'] === $orderId /* your stored order */) {
// mark the order paid (only once)
}
http_response_code(200);Node.js
// Node.js 18+ (global fetch)
const API = 'https://tnetpay.com/api/v1/c2c';
const KEY = process.env.TNETPAY_API_KEY;
async function tnetpay(method, path, body, headers = {}) {
const res = await fetch(API + path, {
method,
headers: { 'X-API-Key': KEY, 'Content-Type': 'application/json', ...headers },
body: body ? JSON.stringify(body) : undefined,
});
const data = await res.json();
if (!res.ok || data.error) throw new Error('TNetPay: ' + (data.error?.code || res.status));
return data;
}
// create + redirect (Express)
app.post('/checkout/:orderId', async (req, res) => {
const p = await tnetpay('POST', '/payments', {
amount: 250000,
orderId: req.params.orderId,
callbackUrl: 'https://shop.example/tnetpay/webhook',
returnUrl: 'https://shop.example/order/' + req.params.orderId,
}, { 'Idempotency-Key': 'order-' + req.params.orderId });
// store p.id on the order
res.redirect(p.paymentUrl);
});
// webhook: confirm with verify before fulfilling
app.post('/tnetpay/webhook', express.json(), async (req, res) => {
const v = await tnetpay('POST', '/payments/' + encodeURIComponent(req.body.id) + '/verify');
if (v.paid) { /* mark order v.orderId as paid, idempotently */ }
res.sendStatus(200);
});Python
import os, requests
API = "https://tnetpay.com/api/v1/c2c"
HEADERS = {"X-API-Key": os.environ["TNETPAY_API_KEY"]}
def tnetpay(method, path, json=None, extra=None):
r = requests.request(method, API + path, json=json, headers={**HEADERS, **(extra or {})}, timeout=20)
data = r.json()
if r.status_code >= 400 or "error" in data:
raise RuntimeError("TNetPay: %s" % (data.get("error", {}).get("code") or r.status_code))
return data
# create + redirect
p = tnetpay("POST", "/payments", {
"amount": 250000,
"orderId": "1001",
"callbackUrl": "https://shop.example/tnetpay/webhook",
"returnUrl": "https://shop.example/order/1001",
}, {"Idempotency-Key": "order-1001"})
redirect_to = p["paymentUrl"]
# webhook handler (Flask)
@app.post("/tnetpay/webhook")
def webhook():
event = request.get_json(force=True)
v = tnetpay("POST", "/payments/%s/verify" % event["id"])
if v["paid"]:
pass # mark order v["orderId"] paid, once
return "", 200