APay Documentationمستندات APay

Everything you need to accept card-to-card payments with automatic SMS confirmation.

هرآنچه برای دریافت پرداخت کارت‌به‌کارت با تأیید خودکار پیامکی لازم است.

Overviewمعرفی

APay is a card-to-card payment gateway for bots and websites. Your customer transfers money directly to your own bank card. The bank sends you a deposit SMS; the APay Forwarder on your phone relays it to APay, which matches it to the order, confirms the payment and calls your webhook.

APay یک درگاه پرداخت کارت‌به‌کارت برای ربات‌ها و سایت‌هاست. مشتری مستقیم به کارت بانکی خودتان پول منتقل می‌کند. بانک پیامک واریز می‌فرستد؛ برنامهٔ APay Forwarder روی گوشی شما آن را به APay می‌رساند و APay آن را با سفارش تطبیق می‌دهد، پرداخت را تأیید می‌کند و وب‌هوک شما را صدا می‌زند.

Customer ──card-to-card──▶ Your bank card
                              │ deposit SMS
                              ▼
                     Your phone (APay Forwarder)
                              │ POST /hook/sms/<key>
                              ▼
Your bot/site ◀── signed webhook ── APay  (match amount → paid → fee)

APay is not a licensed PSP/Shaparak gateway and never holds your funds. It only verifies ordinary card-to-card transfers via SMS.

APay یک درگاه دارای مجوز PSP/شاپرک نیست و پول شما را نگه نمی‌دارد. فقط انتقال‌های کارت‌به‌کارت معمولی را از روی پیامک تأیید می‌کند.

Quick startشروع سریع

  1. Open the APay bot, send /start, accept the terms and complete identity verification (see “Getting access”). After approval the bot sends your API token (shown only once).ربات APay را باز کنید، /start بزنید، قوانین را بپذیرید و احراز هویت را کامل کنید (بخش «دریافت دسترسی»). بعد از تأیید، توکن API در ربات برایتان ارسال می‌شود (فقط یک‌بار نمایش داده می‌شود).
  2. Tap Cards → Add card. Enter a card in your own name and the bank’s SMS sender.روی کارت‌ها ← افزودن کارت بزنید. کارتی به نام خودتان و فرستندهٔ پیامک بانک را وارد کنید.
  3. Install APay Forwarder (Android) or create the iOS Shortcut, and connect it with the device webhook URL.برنامهٔ APay Forwarder (اندروید) را نصب کنید یا شورتکات آیفون را بسازید و با آدرس وب‌هوک دستگاه وصلش کنید.
  4. Top up your wallet from the bot (Top up wallet). Each successful payment costs a flat 1,000 Toman.کیف پول را از ربات شارژ کنید (شارژ کیف پول). به‌ازای هر پرداخت موفق ۱٬۰۰۰ تومان (ثابت) کسر می‌شود.
  5. Create a payment with the API and send the customer to pay_url. Handle the webhook — see “Connect your Telegram bot” and “Connect your website”.با API پرداخت بسازید و مشتری را به pay_url بفرستید. وب‌هوک را دریافت کنید — بخش‌های «اتصال به ربات تلگرام» و «اتصال به سایت».
curl -X POST https://apaygate.shop/api/v1/payments \
  -H "Authorization: Bearer apay_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"amount": 1000000, "order_id": "A-1001"}'

Getting access (verification)دریافت دسترسی (احراز هویت)

To prevent fraud, every merchant is verified before getting an API token. It takes about two minutes in the bot:

برای جلوگیری از تقلب، هر پذیرنده قبل از دریافت توکن API احراز هویت می‌شود. در ربات حدود ۲ دقیقه طول می‌کشد:

  1. Accept the terms of use.قوانین استفاده را بپذیرید.
  2. Share your Iranian SIM number with Telegram’s “share contact” button — a typed or forwarded number is rejected, so the number is proven to be yours.شمارهٔ سیم‌کارت ایرانی خود را با دکمهٔ «اشتراک مخاطب» تلگرام بفرستید — شمارهٔ تایپ‌شده یا فورواردی پذیرفته نمی‌شود؛ پس مالکیت شماره تأیید می‌شود.
  3. Enter your national ID, your full name as on the ID card, and your Jalali birth date.کد ملی، نام و نام خانوادگی (مطابق کارت ملی) و تاریخ تولد شمسی را وارد کنید.
  4. Enter a bank card in your own name.شمارهٔ کارت بانکی به نام خودتان را وارد کنید.
  5. APay checks with official inquiry services that the SIM belongs to your national ID (Shahkar) and that the card belongs to your national ID. If both match you are approved immediately and the bot sends your token. If the check cannot be completed automatically, the team reviews your request manually.APay از سرویس‌های رسمی استعلام می‌کند که سیم‌کارت به کد ملی شما تعلق دارد (شاهکار) و کارت هم به کد ملی شما است. اگر هر دو مطابق باشد فوراً تأیید می‌شوید و توکن در ربات ارسال می‌شود. اگر استعلام خودکار قطعی نشد، تیم ما درخواست را دستی بررسی می‌کند.

Rules: one account per person; every card must be in your own name; your identity data is stored encrypted and used only for verification and fraud prevention. Accounts used for phishing or any illegal activity are banned.

نکته‌ها: هر شخص فقط یک حساب دارد؛ همهٔ کارت‌ها باید به نام خودتان باشد؛ اطلاعات هویتی رمزنگاری‌شده نگهداری و فقط برای احراز هویت و پیشگیری از تقلب استفاده می‌شود. حساب‌هایی که برای فیشینگ یا هر کار غیرقانونی استفاده شوند مسدود می‌شوند.

Authenticationاحراز هویت

Send your API token in the Authorization header. Tokens are stored hashed; if you lose yours, generate a new one in the bot (the old one stops working immediately).

توکن API را در هدر Authorization بفرستید. توکن‌ها به‌صورت هش ذخیره می‌شوند؛ اگر گم شد در ربات توکن جدید بسازید (توکن قبلی فوراً باطل می‌شود).

Authorization: Bearer apay_xxxxxxxx

All amounts are in Rials. Requests and responses are JSON (UTF-8). Timestamps are Unix seconds.

همهٔ مبالغ به ریال است. درخواست و پاسخ JSON (UTF-8) است. زمان‌ها به‌صورت Unix ثانیه‌اند.

Create a paymentساخت پرداخت

POST/api/v1/payments

FieldفیلدTypeنوعDescriptionتوضیح
amount *integerRials, multiple of 10, between 10,000 and 1,000,000,000.ریال، مضرب ۱۰، بین ۱۰٬۰۰۰ تا ۱٬۰۰۰٬۰۰۰٬۰۰۰.
order_idstring ≤100Your order reference. Unique per account and idempotent: repeating the same order_id with the same amount while the payment is still pending returns the same payment; otherwise 409 duplicate_order.شناسهٔ سفارش شما. در هر حساب یکتا و idempotent است: تکرار همان order_id با همان مبلغ تا وقتی پرداخت باز است همان پرداخت را برمی‌گرداند؛ در غیر این صورت 409 duplicate_order.
descriptionstring ≤200Shown on the payment page.در صفحهٔ پرداخت نمایش داده می‌شود.
callback_urlURLWebhook target for this payment. Overrides your default callback. Must be public http(s).مقصد وب‌هوک این پرداخت؛ جایگزین Callback پیش‌فرض. باید http(s) عمومی باشد.
return_urlURLWhere the payment page sends the customer after success.مشتری پس از موفقیت از صفحهٔ پرداخت به این آدرس برمی‌گردد.
device_idintegerPin the payment to a specific card. Default: the card with the fewest open payments.پرداخت را به یک کارت مشخص وصل می‌کند. پیش‌فرض: کارتی که کمترین پرداخت باز را دارد.
curl -X POST https://apaygate.shop/api/v1/payments \
  -H "Authorization: Bearer apay_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"amount":1000000,"order_id":"A-1001","description":"Order 1001","callback_url":"https://bot.example.com/apay","return_url":"https://shop.example.com/thanks"}'
const res = await fetch('https://apaygate.shop/api/v1/payments', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + process.env.APAY_TOKEN,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ amount: 1000000, order_id: 'A-1001' }),
});
const payment = await res.json();
if (!res.ok) throw new Error(payment.error.code);
console.log(payment.pay_url);
import requests
r = requests.post('https://apaygate.shop/api/v1/payments',
    headers={'Authorization': 'Bearer ' + TOKEN},
    json={'amount': 1000000, 'order_id': 'A-1001'}, timeout=15)
r.raise_for_status()
print(r.json()['pay_url'])
$ch = curl_init('https://apaygate.shop/api/v1/payments');
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ['Authorization: Bearer '.$TOKEN, 'Content-Type: application/json'],
  CURLOPT_POSTFIELDS => json_encode(['amount' => 1000000, 'order_id' => 'A-1001']),
]);
$payment = json_decode(curl_exec($ch), true);
echo $payment['pay_url'];

Response 201:

پاسخ 201:

{
  "id": "pay_k3J9xQ2mLw",
  "order_id": "A-1001",
  "description": "Order 1001",
  "amount": 1000000,
  "pay_amount": 1000010,
  "fee": 10000,
  "status": "pending",
  "late": false,
  "created_at": 1760000000,
  "expires_at": 1760000900,
  "paid_at": null,
  "pay_url": "https://apaygate.shop/pay/pay_k3J9xQ2mLw"
}

The customer must transfer exactly pay_amount (not amount). It can be up to 990 Rials higher so that simultaneous same-price orders stay distinguishable. The hosted pay_url page shows the card, the exact amount and a countdown, and updates by itself.

مشتری باید دقیقاً pay_amount را واریز کند (نه amount). این مبلغ تا ۹۹۰ ریال بیشتر می‌شود تا سفارش‌های هم‌قیمت هم‌زمان از هم تشخیص داده شوند. صفحهٔ pay_url کارت، مبلغ دقیق و شمارش معکوس را نشان می‌دهد و خودکار به‌روز می‌شود.

Status & verifyوضعیت و تأیید

GET/api/v1/payments/:id

POST/api/v1/payments/:id/verify

status is one of pending, paid, expired. verify returns {"verified": true|false, "payment": {…}}. Call it after receiving a webhook, or whenever your server comes back online, before delivering goods.

status یکی از pending، paid، expired است. verify پاسخ {"verified": true|false, "payment": {…}} می‌دهد. بعد از دریافت وب‌هوک یا هر وقت سرور شما دوباره آنلاین شد، قبل از تحویل کالا آن را صدا بزنید.

curl https://apaygate.shop/api/v1/payments/pay_k3J9xQ2mLw/verify -X POST \
  -H "Authorization: Bearer apay_xxxxxxxx"

Wallet & devicesکیف پول و دستگاه‌ها

GET/api/v1/wallet

{ "balance": 4985000, "currency": "IRR" }

GET/api/v1/devices

[ { "id": 1, "label": null, "card_last4": "5674", "platform": "android", "last_seen_at": 1760000123 } ]

Webhooksوب‌هوک‌ها

When a payment is confirmed, APay sends POST to the payment’s callback_url (or your default callback) with this JSON body:

وقتی پرداخت تأیید شد، APay یک POST به callback_url پرداخت (یا Callback پیش‌فرض شما) با این بدنهٔ JSON می‌فرستد:

{ "event": "payment.paid", "payment": { "id": "pay_k3J9xQ2mLw", "order_id": "A-1001", "amount": 1000000, "pay_amount": 1000010, "status": "paid", "paid_at": 1760000420, "late": false, … } }
HeaderهدرValueمقدار
X-APay-TimestampUnix seconds of this delivery attempt.زمان Unix همین تلاش ارسال.
X-APay-Signaturehex( HMAC_SHA256( webhook_secret, timestamp + "." + rawBody ) )
Content-Typeapplication/json

Your webhook secret is shown in the bot profile and the web dashboard. Always verify the signature on the raw body, reject timestamps older than ~5 minutes, then confirm with the verify endpoint.

Webhook secret شما در پروفایل ربات و پنل وب نمایش داده می‌شود. همیشه امضا را روی بدنهٔ خام بررسی کنید، timestampهای قدیمی‌تر از ~۵ دقیقه را رد کنید و بعد با endpoint verify تأیید نهایی بگیرید.

import crypto from 'node:crypto';

export function verifyApay(rawBody, headers, secret) {
  const ts  = headers['x-apay-timestamp'];
  const sig = headers['x-apay-signature'] || '';
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const exp = crypto.createHmac('sha256', secret)
    .update(`${ts}.${rawBody}`).digest('hex');
  return sig.length === exp.length &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(exp));
}
import hmac, hashlib, time

def verify_apay(raw: bytes, headers, secret: str) -> bool:
    ts  = headers.get('x-apay-timestamp', '')
    sig = headers.get('x-apay-signature', '')
    if abs(time.time() - int(ts)) > 300:
        return False
    exp = hmac.new(secret.encode(), ts.encode() + b'.' + raw,
                   hashlib.sha256).hexdigest()
    return hmac.compare_digest(sig, exp)
function verify_apay(string $raw, array $h, string $secret): bool {
  $ts  = $h['HTTP_X_APAY_TIMESTAMP'] ?? '';
  $sig = $h['HTTP_X_APAY_SIGNATURE'] ?? '';
  if (abs(time() - (int)$ts) > 300) return false;
  $exp = hash_hmac('sha256', $ts . '.' . $raw, $secret);
  return hash_equals($exp, $sig);
}

Delivery & retriesارسال و تلاش مجدد

Reply with any 2xx within 10 seconds. Otherwise APay retries with back-off: 10 s, 30 s, 2 min, 10 min, 30 min, 1 h, 3 h, 6 h, 12 h (10 attempts, about 24 hours). Redirects are not followed. Deliveries are at-least-once, so make your handler idempotent (dedupe on payment.id).

ظرف ۱۰ ثانیه با هر کد 2xx پاسخ دهید. در غیر این صورت APay با فاصلهٔ فزاینده دوباره تلاش می‌کند: ۱۰ ث، ۳۰ ث، ۲ د، ۱۰ د، ۳۰ د، ۱ س، ۳ س، ۶ س، ۱۲ س (۱۰ تلاش، حدود ۲۴ ساعت). ریدایرکت دنبال نمی‌شود. ارسال‌ها at-least-once هستند؛ پس handler را idempotent بنویسید (با payment.id تکراری‌ها را حذف کنید).

Connect your Telegram botاتصال به ربات تلگرام شما

Typical flow for a bot that sells something: (1) the user taps “Pay”, (2) your bot calls POST /payments with an order_id and a callback_url, (3) it replies with an inline button to pay_url, (4) APay calls your webhook when the payment is confirmed, (5) your bot verifies the signature, calls verify, and delivers the product. Your bot server must be reachable over HTTPS; if it is offline, APay retries the webhook for about 24 hours.

جریان معمول برای رباتی که چیزی می‌فروشد: (۱) کاربر «پرداخت» را می‌زند، (۲) ربات شما POST /payments را با order_id و callback_url صدا می‌زند، (۳) با یک دکمهٔ شیشه‌ای به pay_url پاسخ می‌دهد، (۴) بعد از تأیید پرداخت، APay وب‌هوک شما را صدا می‌زند، (۵) ربات امضا را بررسی می‌کند، verify را صدا می‌زند و محصول را تحویل می‌دهد. سرور ربات شما باید روی HTTPS در دسترس باشد؛ اگر آفلاین باشد، APay تا حدود ۲۴ ساعت وب‌هوک را دوباره می‌فرستد.

import { Bot, InlineKeyboard } from 'grammy';
import express from 'express';
import crypto from 'node:crypto';

const bot = new Bot(process.env.BOT_TOKEN);
const API = 'https://apaygate.shop/api/v1';
const H = { Authorization: 'Bearer ' + process.env.APAY_TOKEN, 'Content-Type': 'application/json' };
const orders = new Map(); // order_id -> chat_id (in production: a database)

bot.command('buy', async (ctx) => {
  const orderId = 'T' + Date.now();
  const r = await fetch(API + '/payments', { method: 'POST', headers: H,
    body: JSON.stringify({ amount: 500000, order_id: orderId, callback_url: process.env.PUBLIC_URL + '/apay' }) });
  const p = await r.json();
  if (!r.ok) return ctx.reply('Error: ' + p.error.code);
  orders.set(orderId, ctx.chat.id);
  await ctx.reply('Pay 50,000 Toman', { reply_markup: new InlineKeyboard().url('💳 Pay', p.pay_url) });
});

const app = express();
app.post('/apay', express.raw({ type: '*/*' }), async (req, res) => {
  const ts = req.get('x-apay-timestamp'), sig = req.get('x-apay-signature') || '';
  const exp = crypto.createHmac('sha256', process.env.APAY_SECRET).update(ts + '.' + req.body).digest('hex');
  if (sig.length !== exp.length || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(exp))) return res.sendStatus(401);
  const { payment } = JSON.parse(req.body);
  const v = await (await fetch(API + '/payments/' + payment.id + '/verify', { method: 'POST', headers: H })).json();
  const chat = orders.get(payment.order_id);
  if (v.verified && chat) { orders.delete(payment.order_id); await bot.api.sendMessage(chat, '✅ Payment confirmed'); }
  res.sendStatus(200);
});
app.listen(3000);
bot.start();
import hmac, hashlib, json, os, requests
from flask import Flask, request

API = 'https://apaygate.shop/api/v1'
H = {'Authorization': 'Bearer ' + os.environ['APAY_TOKEN']}
TG = 'https://api.telegram.org/bot' + os.environ['BOT_TOKEN']

def create_payment(chat_id, amount_rial, order_id):
    r = requests.post(API + '/payments', headers=H, timeout=15, json={
        'amount': amount_rial, 'order_id': order_id,
        'callback_url': os.environ['PUBLIC_URL'] + '/apay'})
    r.raise_for_status()
    p = r.json()
    requests.post(TG + '/sendMessage', json={
        'chat_id': chat_id, 'text': 'Tap to pay:',
        'reply_markup': {'inline_keyboard': [[{'text': '💳 Pay', 'url': p['pay_url']}]]}})

app = Flask(__name__)

@app.post('/apay')
def apay():
    raw = request.get_data()
    ts = request.headers['X-APay-Timestamp']
    sig = request.headers['X-APay-Signature']
    exp = hmac.new(os.environ['APAY_SECRET'].encode(), ts.encode() + b'.' + raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(sig, exp):
        return '', 401
    pay = json.loads(raw)['payment']
    v = requests.post(API + '/payments/' + pay['id'] + '/verify', headers=H, timeout=15).json()
    if v.get('verified'):
        pass  # deliver the order pay['order_id']
    return '', 200

Use your own API token (APAY_TOKEN) and webhook secret (APAY_SECRET, shown in your bot profile). Keep them in environment variables — never in the code or in the chat. Make the handler idempotent: the same payment may be delivered more than once.

از توکن API (APAY_TOKEN) و webhook secret (APAY_SECRET، در پروفایل ربات) خودتان استفاده کنید. آن‌ها را در متغیر محیطی نگه دارید، نه در کد و نه در چت. handler را idempotent بنویسید: ممکن است یک پرداخت بیش از یک‌بار ارسال شود.

Connect your websiteاتصال به سایت

Create the payment on your server when the customer checks out, redirect them to pay_url, and mark the order as paid only from the verified webhook — never from the redirect back to your site.

هنگام تسویه، پرداخت را روی سرور خودتان بسازید، مشتری را به pay_url هدایت کنید و سفارش را فقط از وب‌هوک تأییدشده «پرداخت‌شده» کنید — هرگز فقط با برگشتن مشتری به سایت.

<?php
// checkout.php — create a payment and redirect
$ch = curl_init('https://apaygate.shop/api/v1/payments');
curl_setopt_array($ch, [
  CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('APAY_TOKEN'), 'Content-Type: application/json'],
  CURLOPT_POSTFIELDS => json_encode([
    'amount' => $rial, 'order_id' => $orderId,
    'callback_url' => 'https://shop.example.com/apay.php',
    'return_url' => 'https://shop.example.com/thanks',
  ]),
]);
$p = json_decode(curl_exec($ch), true);
header('Location: ' . $p['pay_url']);

// apay.php — webhook
$raw = file_get_contents('php://input');
$ts  = $_SERVER['HTTP_X_APAY_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_APAY_SIGNATURE'] ?? '';
if (abs(time() - (int)$ts) > 300 || !hash_equals(hash_hmac('sha256', $ts . '.' . $raw, getenv('APAY_SECRET')), $sig)) { http_response_code(401); exit; }
$payment = json_decode($raw, true)['payment'];
// call POST /payments/{id}/verify, check amount and order_id against your order, then mark it paid
http_response_code(200);
app.post('/checkout', async (req, res) => {
  const r = await fetch('https://apaygate.shop/api/v1/payments', {
    method: 'POST',
    headers: { Authorization: 'Bearer ' + process.env.APAY_TOKEN, 'Content-Type': 'application/json' },
    body: JSON.stringify({ amount: order.rial, order_id: order.id,
      callback_url: 'https://shop.example.com/apay', return_url: 'https://shop.example.com/thanks' }),
  });
  const p = await r.json();
  res.redirect(p.pay_url);
});
// the webhook handler is the same as in “Connect your Telegram bot”

Payment lifecycleچرخهٔ عمر پرداخت

StatusوضعیتMeaningمعنی
pendingWaiting for the transfer. Valid for 15 minutes by default.منتظر واریز. به‌طور پیش‌فرض ۱۵ دقیقه معتبر است.
paidA matching deposit SMS arrived. Fee deducted, webhook queued.پیامک واریز منطبق رسید. کارمزد کسر و وب‌هوک در صف قرار گرفت.
expiredTime ran out without a matching SMS.زمان بدون پیامک منطبق تمام شد.

Late SMS: bank SMS can be delayed. For 10 minutes after expiry APay still matches a deposit to an expired payment and marks it paid with "late": true.

پیامک دیرهنگام: پیامک بانک ممکن است دیر برسد. تا ۱۰ دقیقه بعد از انقضا هم APay واریز را به پرداخت منقضی تطبیق می‌دهد و آن را با "late": true در وضعیت paid قرار می‌دهد.

Replay protection: an SMS older than the payment’s creation time can never confirm it, and every SMS from the Android app carries a unique id, so a re-sent message never confirms a second payment.

محافظت در برابر بازپخش: پیامکی که قدیمی‌تر از زمان ساخت پرداخت باشد هرگز آن را تأیید نمی‌کند و هر پیامک اپ اندروید شناسهٔ یکتا دارد؛ پس پیامک ارسال‌مجدد پرداخت دوم را تأیید نمی‌کند.

Unique amountsمبلغ یکتا

Card-to-card transfers carry no order reference, so APay identifies a payment by amount + card. If another open payment on the same card already uses the amount, the new one gets amount + 10, + 20 … up to + 990 Rials. At most 100 same-price payments can be open on one card; beyond that the API returns 429 busy. Add more cards to scale out — APay spreads load across your cards.

انتقال کارت‌به‌کارت شناسهٔ سفارش ندارد، پس APay پرداخت را با مبلغ + کارت می‌شناسد. اگر پرداخت باز دیگری روی همان کارت همان مبلغ را داشته باشد، پرداخت جدید amount + 10، + 20 … تا + 990 ریال می‌گیرد. حداکثر ۱۰۰ پرداخت هم‌قیمت روی یک کارت هم‌زمان باز می‌ماند؛ بیشتر از آن API پاسخ 429 busy می‌دهد. برای مقیاس‌پذیری کارت بیشتر اضافه کنید — APay بار را بین کارت‌ها پخش می‌کند.

Wallet & feesکیف پول و کارمزد

Fees are charged from your prepaid wallet only when a payment succeeds. The fee is a flat 1,000 Toman (10,000 Rials) per successful card-to-card payment, whatever the amount. Creating a payment requires a balance of at least the fee; the fee is deducted at confirmation. Top up in the bot (Top up wallet): you transfer to APay’s own card and the wallet is credited automatically — the same mechanism you sell.

کارمزد فقط وقتی پرداخت موفق شود از کیف پول پیش‌پرداخت کسر می‌شود. کارمزد به‌ازای هر پرداخت کارت‌به‌کارت موفق مبلغ ثابت ۱٬۰۰۰ تومان (۱۰٬۰۰۰ ریال) است، فارغ از مقدار پرداخت. ساخت پرداخت نیازمند موجودی حداقل برابر کارمزد است و کارمزد هنگام تأیید کسر می‌شود. شارژ از ربات (شارژ کیف پول): به کارت خود APay واریز می‌کنید و کیف پول خودکار شارژ می‌شود — همان مکانیزمی که شما می‌فروشید.

Your balance is available through GET /api/v1/wallet. If it is lower than the fee, POST /api/v1/payments returns 402 insufficient_balance.

موجودی از GET /api/v1/wallet قابل دریافت است. اگر کمتر از کارمزد باشد، POST /api/v1/payments خطای 402 insufficient_balance می‌دهد.

Android appاپ اندروید

APay Forwarder listens for incoming SMS and posts each one to your device webhook. Download the APK from https://apaygate.shop/download/apay-forwarder.apk, allow SMS permission, then connect it:

APay Forwarder پیامک‌های ورودی را دریافت و هرکدام را به وب‌هوک دستگاه شما ارسال می‌کند. APK را از https://apaygate.shop/download/apay-forwarder.apk دانلود کنید، دسترسی پیامک را بدهید و وصلش کنید:

  • Open the pair link that the bot gives you (https://apaygate.shop/pair/<key>) on the phone and tap “Open in app” — or paste the webhook URL into the app.لینک اتصال ربات (https://apaygate.shop/pair/<key>) را روی گوشی باز کنید و «باز کردن در اپ» را بزنید — یا آدرس وب‌هوک را داخل اپ بچسبانید.
  • Enter the bank’s SMS sender(s) in the filter, tap Save, then Test connection.فرستندهٔ پیامک بانک را در فیلتر وارد کنید، ذخیره و بعد تست اتصال را بزنید.
  • Disable battery optimisation / allow auto-start for the app (Xiaomi, Samsung, Huawei, … kill background receivers otherwise).بهینه‌سازی باتری را برای اپ خاموش و auto-start را روشن کنید (در شیائومی، سامسونگ، هوآوی و … وگرنه گیرندهٔ پیامک بسته می‌شود).

Offline phone: each SMS is stored in a persistent queue and sent as soon as the phone has internet, with exponential back-off. A heartbeat every ~15 minutes tells APay the phone is alive.

گوشی آفلاین: هر پیامک در صف ماندگار ذخیره می‌شود و به‌محض اتصال اینترنت با backoff نمایی ارسال می‌شود. ضربان هر ~۱۵ دقیقه به APay خبر می‌دهد گوشی زنده است.

iPhone (Shortcuts)آیفون (Shortcuts)

  1. Open Shortcuts → Automation → New Automation → Message.Shortcuts ← Automation ← New Automation ← Message را باز کنید.
  2. Sender: choose the bank’s number/contact. Select Run Immediately.Sender: شماره/مخاطب بانک را انتخاب و Run Immediately را فعال کنید.
  3. Add the action Get Contents of URL: URL = your device webhook, Method = POST, Request Body = JSON.عمل Get Contents of URL را اضافه کنید: URL = وب‌هوک دستگاه، Method = POST، Request Body = JSON.
  4. Add two fields: sender = Sender and message = Message (magic variables from the trigger). Turn off “Notify When Run”.دو فیلد اضافه کنید: sender = Sender و message = Message (متغیرهای جادویی تریگر). «Notify When Run» را خاموش کنید.
POST https://apaygate.shop/hook/sms/<device_key>
Content-Type: application/json

{ "sender": "<Sender>", "message": "<Message>" }

iOS does not queue failed requests. If the phone is offline or off when the SMS arrives, that SMS is not delivered later. For unattended or high-volume use, prefer an Android phone.

iOS درخواست‌های ناموفق را در صف نگه نمی‌دارد. اگر هنگام رسیدن پیامک گوشی آفلاین یا خاموش باشد، آن پیامک بعداً ارسال نمی‌شود. برای استفادهٔ بی‌نظارت یا حجم بالا، گوشی اندروید را ترجیح دهید.

SMS webhookوب‌هوک پیامک

POST/hook/sms/:device_key

Used by the Android app and the iOS Shortcut; you can also call it from your own forwarder. JSON, form-encoded and query parameters are accepted. The device key is a secret — treat it like a password.

توسط اپ اندروید و شورتکات آیفون استفاده می‌شود؛ می‌توانید از فوروارد‌کنندهٔ خودتان هم صدایش بزنید. JSON، form و query پذیرفته می‌شود. کلید دستگاه محرمانه است — مثل رمز با آن رفتار کنید.

FieldفیلدAliasesنام‌های دیگرDescriptionتوضیح
message *text, body, content, sms, msgSMS text.متن پیامک.
senderfrom, address, number, phoneSender; checked against the card’s allowed senders.فرستنده؛ با فرستنده‌های مجاز کارت مقایسه می‌شود.
uididUnique id of this SMS. Same uid is processed once, forever.شناسهٔ یکتای این پیامک. یک uid فقط یک‌بار پردازش می‌شود.
tstimestamp, dateTime the phone received the SMS (Unix seconds).زمان دریافت پیامک روی گوشی (Unix ثانیه).

Response: {"ok": true, "result": "…", "payment_id": "…"|null} where result is:

پاسخ: {"ok": true, "result": "…", "payment_id": "…"|null} که result یکی از این‌هاست:

resultMeaningمعنی
matchedA pending payment was confirmed.یک پرداخت در انتظار تأیید شد.
unmatchedA deposit, but no open payment has that amount.واریز است اما پرداخت بازی با این مبلغ نیست.
not_depositNot a deposit (withdrawal, OTP, ad…).واریز نیست (برداشت، رمز پویا، تبلیغ…).
ignored_senderSender is not in the card’s allow-list.فرستنده در فهرست مجاز کارت نیست.
duplicateAlready processed.قبلاً پردازش شده است.

GET / POST/hook/ping/:device_key

Heartbeat: updates “last seen” for the device.

ضربان: «آخرین مشاهده» دستگاه را به‌روز می‌کند.

Bot guideراهنمای ربات

Command / buttonدستور / دکمهWhat it doesکاربرد
/startSign up and receive your API token.ثبت‌نام و دریافت توکن API.
💳 Cardsکارت‌هاAdd / remove cards, see pairing instructions and last SMS time.افزودن/حذف کارت، دستورالعمل اتصال و زمان آخرین پیامک.
💰 Top up walletشارژ کیف پولCreate a top-up payment to APay’s card.ساخت پرداخت شارژ به کارت APay.
👤 ProfileپروفایلBalance, default callback, webhook secret.موجودی، Callback پیش‌فرض، webhook secret.
🔑 API tokenتوکن APIGenerate a new token (revokes the old one).ساخت توکن جدید (توکن قبلی باطل می‌شود).
🌐 Web panelپنل وبOne-time sign-in link (10 minutes) to the dashboard.لینک ورود یک‌بارمصرف (۱۰ دقیقه) به پنل.
/setcallback <url>Default webhook URL for your payments.آدرس وب‌هوک پیش‌فرض پرداخت‌های شما.
/cancelCancel the current step.لغو مرحلهٔ جاری.

Errorsخطاها

Errors use the HTTP status plus a JSON body. Rely on code; message is a human-readable Persian text.

خطاها با کد HTTP و بدنهٔ JSON برمی‌گردند. به code تکیه کنید؛ message متن خوانای فارسی است.

{ "error": { "code": "insufficient_balance", "message": "…" } }
HTTPcodeMeaningمعنی
400invalid_amountNot an integer multiple of 10 within limits.عدد صحیح مضرب ۱۰ در محدودهٔ مجاز نیست.
400invalid_urlcallback_url / return_url is not http(s).callback_url / return_url معتبر http(s) نیست.
400bad_body · no_messageMalformed JSON / SMS text missing.JSON نامعتبر / متن پیامک ناموجود.
401unauthorizedMissing or wrong token.توکن ناموجود یا اشتباه.
402insufficient_balanceWallet balance is below the fee.موجودی کیف پول از کارمزد کمتر است.
403suspendedAccount is disabled or banned.حساب غیرفعال یا مسدود است.
403not_verifiedIdentity verification is not complete.احراز هویت کامل نشده است.
404not_found · device_not_foundUnknown payment / device.پرداخت/دستگاه ناشناخته.
409duplicate_orderorder_id already used.order_id قبلاً استفاده شده.
409no_deviceNo active, verified card. Add a card in your own name first.کارت فعال و تأییدشده‌ای ندارید. ابتدا کارتی به نام خودتان اضافه کنید.
413too_largeBody larger than 64 KB.بدنه بزرگ‌تر از ۶۴ کیلوبایت.
429busy100 same-price payments already open on the card.۱۰۰ پرداخت هم‌قیمت روی کارت باز است.
429rate_limitedToo many requests.درخواست بیش از حد.
500internalServer error — retry later.خطای سرور — بعداً دوباره تلاش کنید.

Limitsمحدودیت‌ها

ItemموردDefaultپیش‌فرض
Amount per paymentمبلغ هر پرداخت10,000 – 1,000,000,000 Rials۱۰٬۰۰۰ تا ۱٬۰۰۰٬۰۰۰٬۰۰۰ ریال
Payment validityاعتبار پرداخت15 minutes (+10 min late-SMS grace)۱۵ دقیقه (+۱۰ دقیقه مهلت پیامک دیرهنگام)
Open same-price payments per cardپرداخت هم‌قیمت باز روی هر کارت100
API rate limitمحدودیت نرخ API300 requests / minute / IP۳۰۰ درخواست در دقیقه برای هر IP
SMS webhook rate limitمحدودیت نرخ وب‌هوک پیامک120 requests / minute / IP۱۲۰ درخواست در دقیقه برای هر IP
Request bodyاندازهٔ بدنه64 KB
Webhook timeout / retriesمهلت و تلاش وب‌هوک10 s · 10 attempts over ~24 h۱۰ ثانیه · ۱۰ تلاش در حدود ۲۴ ساعت

Operators can change these values in the server configuration.

مدیر سرور می‌تواند این مقادیر را در تنظیمات سرور تغییر دهد.

Security checklistفهرست امنیت

  • Set the SMS sender on every card. Anyone can text your phone “deposit 1,000,000”; only the bank’s sender should count.فرستندهٔ پیامک را روی هر کارت تنظیم کنید. هر کسی می‌تواند به گوشی شما پیامک «واریز ۱٬۰۰۰٬۰۰۰» بفرستد؛ فقط فرستندهٔ بانک باید معتبر باشد.
  • Keep the API token, webhook secret and device webhook URL private; never put them in client-side code.توکن API، webhook secret و آدرس وب‌هوک دستگاه را محرمانه نگه دارید؛ هرگز در کد سمت کاربر نگذارید.
  • Always verify the webhook signature and call verify before delivering goods.همیشه امضای وب‌هوک را بررسی کنید و قبل از تحویل کالا verify را صدا بزنید.
  • Match payment.amount and order_id with your own order record — never trust amounts from the customer.payment.amount و order_id را با سفارش خودتان تطبیق دهید — هرگز مبلغ اعلامی مشتری را باور نکنید.
  • Callback URLs must be public; private/internal addresses are blocked (SSRF protection).آدرس Callback باید عمومی باشد؛ آدرس‌های خصوصی/داخلی مسدود است (محافظت SSRF).
  • Rotate the token from the bot if you suspect a leak.در صورت شک به نشت، توکن را از ربات عوض کنید.
  • Do not share your account or token with others — you are responsible for everything done through them; accounts used for fraud are banned.حساب و توکن را به کسی ندهید — مسئولیت هر کاری که با آن‌ها انجام شود با شماست و حساب‌های مرتبط با تقلب مسدود می‌شوند.

FAQسؤالات متداول

My server was offline — did I lose the payment?سرورم آفلاین بود — پرداخت از دست رفت؟

No. APay confirmed it when the SMS arrived and retries your webhook for ~24 hours. You can also call verify or GET /payments/:id any time.

خیر. APay همان لحظهٔ رسیدن پیامک تأییدش کرده و وب‌هوک را حدود ۲۴ ساعت دوباره می‌فرستد. هر زمان هم می‌توانید verify یا GET /payments/:id را صدا بزنید.

The customer paid a different amount.مشتری مبلغ دیگری واریز کرد.

The payment stays pending and later expired; the SMS is logged as unmatched. Resolve it with the customer manually.

پرداخت pending می‌ماند و بعد expired می‌شود؛ پیامک با وضعیت unmatched ثبت می‌شود. موضوع را دستی با مشتری حل کنید.

Can I use several cards?می‌توانم چند کارت داشته باشم؟

Yes. Add as many as you like; APay assigns each new payment to the card with the fewest open payments.

بله. هر تعداد بخواهید اضافه کنید؛ APay هر پرداخت جدید را به کارتی با کمترین پرداخت باز می‌دهد.

Does APay hold my money?آیا APay پول مرا نگه می‌دارد؟

No. Customers pay your card directly. The wallet only holds prepaid fees.

خیر. مشتری مستقیم به کارت شما واریز می‌کند. کیف پول فقط کارمزد پیش‌پرداخت را نگه می‌دارد.

Is there a sandbox?محیط آزمایشی دارید؟

Use a small real amount (min 10,000 Rials) with your own card, or send a test SMS to your device webhook.

با کارت خودتان مبلغ کوچک واقعی (حداقل ۱۰٬۰۰۰ ریال) بزنید یا یک پیامک آزمایشی به وب‌هوک دستگاه بفرستید.