زویکس APIقیمت زنده طلا، سکه و ارز

راهنما

استفاده از API قیمت در اپ اندروید

کلید مخصوص اپ اندروید، محدود به نام بسته و اثر انگشت گواهی امضا؛ با Interceptor آماده برای OkHttp و Retrofit، کش و ارسال زنده.

دو راه، بسته به حساسیت

روشمناسب برایامنیت
اپ ← سرور شما ← API زویکساپ‌های پرمخاطب، کلید پلن گران، داده حساسبالاترین: کلید هرگز در APK نیست؛ کلید سرور را به IP قفل کنید
اپ مستقیم با کلید «اپ اندروید»اپ‌های ساده، ویجت صفحه اصلی، ماشین‌حساب طلاخوب: کلید فقط برای اپ شما (نام بسته + گواهی امضا) کار می‌کند

۱. ساخت کلید اندروید

  • در داشبورد ← کلیدهای API ← کلید جدید، نوع «اپ اندروید» را انتخاب کنید.
  • هر خط یک اپ: نام‌بسته;SHA-256، مثل ir.example.goldprice;AB:CD:…:89. فقط نام بسته (بدون گواهی) هم پذیرفته می‌شود ولی ضعیف‌تر است.
  • اگر اپ را با Play App Signing منتشر می‌کنید، SHA-256 «گواهی امضای اپ» را از Play Console (Setup ← App signing) بردارید، نه گواهی آپلود. برای نسخه debug یک خط جدا اضافه کنید.
اثر انگشت SHA-256 گواهی
# امضای debug (برای کلید آزمایشی)
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android | grep SHA256

# امضای release خودتان
keytool -list -v -keystore release.jks -alias my-alias | grep SHA256

# با Gradle: همه امضاها
./gradlew signingReport

۲. هدرهای هر درخواست

هدرمقدار
X-API-Keyکلید اندروید شما
X-Android-Packageنام بسته اپ (context.packageName)
X-Android-CertSHA-256 گواهی امضای اپ؛ با یا بدون «:»، حروف بزرگ یا کوچک

اگر هدرها نیایند یا با اپ ثبت‌شده نخوانند، پاسخ 403 app_not_allowed است.

۳. Interceptor آماده برای OkHttp

این کلاس نام بسته و گواهی را در زمان اجرا از خود اپ می‌خواند؛ پس نسخه دستکاری‌شده و دوباره‌امضاشده اپ، گواهی دیگری می‌فرستد و رد می‌شود.

ZevixInterceptor.kt
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import okhttp3.Interceptor
import okhttp3.Response
import java.security.MessageDigest

/** کلید API و شناسه اپ (نام بسته و SHA-256 گواهی امضا) را به هر درخواست اضافه می‌کند. */
class ZevixInterceptor(context: Context, private val apiKey: String) : Interceptor {
    private val pkg = context.packageName
    private val cert = signingCertSha256(context)

    override fun intercept(chain: Interceptor.Chain): Response = chain.proceed(
        chain.request().newBuilder()
            .header("X-API-Key", apiKey)
            .header("X-Android-Package", pkg)
            .header("X-Android-Cert", cert)
            .build()
    )

    private fun signingCertSha256(context: Context): String {
        val pm = context.packageManager
        val signatures = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            pm.getPackageInfo(pkg, PackageManager.GET_SIGNING_CERTIFICATES)
                .signingInfo!!.apkContentsSigners
        } else {
            @Suppress("DEPRECATION")
            pm.getPackageInfo(pkg, PackageManager.GET_SIGNATURES).signatures!!
        }
        val digest = MessageDigest.getInstance("SHA-256").digest(signatures[0].toByteArray())
        return digest.joinToString(":") { "%02X".format(it) }
    }
}
Retrofit
// یک‌بار، مثلاً در Application یا ماژول Hilt
val client = OkHttpClient.Builder()
    .addInterceptor(ZevixInterceptor(appContext, BuildConfig.ZEVIX_API_KEY))
    .cache(Cache(File(appContext.cacheDir, "zevix"), 1L * 1024 * 1024))
    .build()

interface ZevixApi {
    @GET("v1/prices")
    suspend fun prices(@Query("symbols") symbols: String): PricesResponse
}

val api = Retrofit.Builder()
    .baseUrl("https://data.zevix.ir/")
    .client(client)
    .addConverterFactory(GsonConverterFactory.create())
    .build()
    .create(ZevixApi::class.java)

// در ViewModel
val board = api.prices("gold_18,coin_emami,usd").data

۴. ارسال زنده در اپ

برای تابلوی قیمت زنده به‌جای درخواست هر چند ثانیه، یک اتصال /v1/stream باز کنید (پلن‌های دارای ارسال زنده). وقتی اپ به پس‌زمینه رفت اتصال را ببندید و هنگام بازگشت دوباره باز کنید.

Server-Sent Events
// ارسال زنده با okhttp-sse (implementation("com.squareup.okhttp3:okhttp-sse:4.12.0"))
val request = Request.Builder().url("https://data.zevix.ir/v1/stream?symbols=gold_18,usd").build()
EventSources.createFactory(client).newEventSource(request, object : EventSourceListener() {
    override fun onEvent(source: EventSource, id: String?, type: String?, data: String) {
        if (type == "prices") updateBoard(JSONObject(data).getJSONArray("quotes"))
    }
    override fun onFailure(source: EventSource, t: Throwable?, response: okhttp3.Response?) {
        // قطع شبکه: چند ثانیه بعد دوباره وصل شوید
    }
})

محدودیت امنیتی را بشناسید

هر کلیدی که داخل APK باشد با مهندسی معکوس قابل استخراج است و هدرها را می‌شود جعل کرد. محدودیت اپ جلوی استفاده سرسری از کلید را می‌گیرد، نه یک مهاجم مصمم. برای پلن‌های گران یا اپ پرمخاطب، API را از سرور خودتان صدا بزنید و قیمت را برای کاربران اپ کش کنید.

نکته‌ها

  • هر اپ (و هر محیط debug/release) کلید جدا داشته باشد تا مصرفش جدا دیده شود و در صورت لو رفتن فقط همان باطل شود.
  • پاسخ را چند ثانیه کش کنید؛ هزاران کاربر هم‌زمان با درخواست هر ثانیه، سقف پلن را پر می‌کنند.
  • خطای 429 را با صبر به اندازه هدر Retry-After مدیریت کنید و 402 subscription_expired را به کاربر «سرویس موقتاً در دسترس نیست» نشان دهید.