search

Fetch Device Details

Third party applications running on the Paytm EDC device can read the device’s last-known location, device identifiers and backend App Notifications through a read-only Android ContentProvider exposed by the Paytm Payments app.

Authority : com.paytm.pos.deviceinfoprovider

Permission required in your AndroidManifest : <uses-permission android:name="com.paytm.pos.provider.DEVICE"/> (protection level normal, no runtime grant needed)

The provider is read-only. insert(), update() and delete() throw UnsupportedOperationException, and getType() always returns null.

Path

Returns

Projection

/location

Latitude, longitude, accuracy, last-update time

Ignored; all four columns are always returned

/deviceInfo

TID, MID, serial number, SIM number, STAN

Mandatory; only the requested columns are returned

/app_notification/{appPkg}

Queued App Notification payloads addressed to your app package {appPkg}

Ignored; fixed five-column cursor

 

Location ("/location")

Location of the device is asynchronously updated information. Each query first asks the Payments app to refresh the fix if it is stale, then returns whatever is currently cached. The provider returns a single row with the following columns:

Column

Description

latitude

Latitude of the last-known position

longitude

Longitude of the last-known position

accuracy

Accuracy in metres

lastUpdateTime

Epoch timestamp of the last GPS fix

Sample code :

val uri = Uri.parse("content://com.paytm.pos.deviceinfoprovider/location")
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
    if (cursor.moveToFirst()) {
        fun col(name: String): String {
            val i = cursor.getColumnIndex(name)
            return if (i >= 0) cursor.getString(i) ?: "" else ""
        }
        val lat = col("latitude")
        val long = col("longitude")
        val acc = col("accuracy")
        var lastUpdate = col("lastUpdateTime")
        if (lastUpdate.isNotBlank()) lastUpdate = getDate(lastUpdate.toLong())
    }
}

 

Device Information ("/deviceInfo")

Upon activation of the device, device information can be requested from the provider. A projection is mandatory: pass the column names you need and only those columns are populated. If the projection is null or empty the query returns null.

Column

Description

tid

Terminal ID assigned during activation

mid

Merchant ID associated with this terminal

serialNumber

Hardware serial number of the device

simNumber

ICCID (SIM serial number) of the inserted SIM. Empty string when unavailable (no SIM inserted, or the SIM serial could not be read on this device).

stan

6-digit System Trace Audit Number, zero-padded. See the note below before requesting it.

Note : Requesting "stan" has a side effect. Every query whose projection includes stan consumes the next number from the terminal’s shared, monotonically increasing STAN counter (wraps at 900000 and restarts at 000001). This is the same counter the Payments app uses for its own transactions. Include stan only when you actually need a unique trace number, never in a general "read device info" call.

Sample code :

val uri = Uri.parse("content://com.paytm.pos.deviceinfoprovider/deviceInfo")
// Request only what you need. Add "stan" only when a fresh trace number is required.
val projection = arrayOf("tid", "mid", "serialNumber", "simNumber")
contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
    if (cursor.moveToFirst()) {
        fun col(name: String): String {
            val i = cursor.getColumnIndex(name)
            return if (i >= 0) cursor.getString(i) ?: "" else ""
        }
        val tid = col("tid")
        val mid = col("mid")
        val sNo = col("serialNumber")
        val simNo = col("simNumber")
    }
}

 

App Notification ("/app_notification/{appPkg}")

The Paytm backend can push an App Notification to the terminal addressed to a specific third party app package. The Payments app does not print, play a sound or show any UI for these messages. It stores the raw actionInfo JSON locally and notifies the destination app through this provider. The destination app then reads the queued rows on its own schedule.

URI :

content://com.paytm.pos.deviceinfoprovider/app_notification/{appPkg}
content://com.paytm.pos.deviceinfoprovider/app_notification/{appPkg}?includeRead=true

{appPkg} is the caller’s own package name. The provider verifies that the path segment matches one of the packages owned by the calling UID. On mismatch the query returns an empty cursor (no rows are leaked and nothing is marked read). A blank package segment also returns an empty cursor.

Columns : the cursor always has these five columns, in this order. Rows are returned oldest-first by receivedTs.

Column

Type

Description

_id

long

Row id in the Payments app database

appPkg

String

Destination package (equals {appPkg})

reqTimestamp

String

Backend-supplied request timestamp. appPkg + reqTimestamp uniquely identify a notification; a repeat delivery replaces the earlier row instead of adding a duplicate

receivedTs

long

Epoch millis when the terminal received the message

payload

String

Raw actionInfo JSON, passed through unchanged

Read semantics :

  • Default (no query parameter) : returns unread rows only.
  • ?includeRead=true : returns both read and unread rows. Any other value, or the parameter being absent, behaves as the default.
  • In either mode the query atomically marks every returned unread row as read in the same database transaction. A second default query returns nothing until a new notification arrives. Treat each default query as a one-shot drain and persist what you need on your side.
  • Rows are retained for 24 hours from receivedTs. Cleanup runs when the next notification is stored, not on read, so a row older than 24 hours may still be returned until the next arrival.

Getting notified of new rows : when a new notification is stored, the Payments app calls ContentResolver.notifyChange() on the per-package URI (without the query parameter). Register a ContentObserver on that URI and query inside onChange(). Also query once on startup to pick up anything that arrived while your app was not running.

val uri = Uri.parse("content://com.paytm.pos.deviceinfoprovider/app_notification/$packageName")

val observer = object : ContentObserver(Handler(Looper.getMainLooper())) {
    override fun onChange(selfChange: Boolean, changedUri: Uri?) {
        drainNotifications()
    }
}
contentResolver.registerContentObserver(uri, false, observer)
drainNotifications() // catch up on anything received while the app was down

fun drainNotifications() {
    contentResolver.query(uri, null, null, null, null)?.use { cursor ->
        while (cursor.moveToNext()) {
            val reqTimestamp = cursor.getString(cursor.getColumnIndexOrThrow("reqTimestamp"))
            val receivedTs = cursor.getLong(cursor.getColumnIndexOrThrow("receivedTs"))
            val payload = cursor.getString(cursor.getColumnIndexOrThrow("payload"))
            handle(reqTimestamp, receivedTs, payload)
        }
    }
}

// Remember to unregister when done:
// contentResolver.unregisterContentObserver(observer)

To re-read rows that were already consumed (for example after a crash during processing), query with ?includeRead=true. Rows older than 24 hours may no longer be present.