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.