Developer dashboard

Merchant PIN Verification#

API reference for verifying the merchant's PIN before performing a void or refund on the mobile POS app.


Base URL#

text
https://uat.auxcore.net

Endpoint#

POST/api/v1/merchants/verify-pin

When to Call#

Call this endpoint immediately before submitting a void or refund request. The merchant enters their 6-digit PIN on the app; the app calls this endpoint to verify it before proceeding. Do not cache the result — call it fresh each time a void or refund is initiated.


Authentication#

Header Required Description
Authorization Yes Bearer <user_jwt> — the merchant's user JWT from login. The PIN checked is that of the merchant the JWT is scoped to
X-Tenant-ID Yes Tenant identifier (same value used on all other requests)
Content-Type Yes application/json
X-Device-Token No Device JWT from registration. If provided and valid, it is validated. If provided and invalid or revoked, the request is rejected with 401. If omitted, the request proceeds on user JWT alone.

Note: The device token is optional. When sent, it is only checked for validity (live session, device not suspended) — it does not change which merchant's PIN is verified, and it is not compared with the merchant in the user JWT. When calling from the portal (web), omit it.


Request#

Headers#

text
POST /api/v1/merchants/verify-pin HTTP/1.1
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
X-Tenant-ID: tenant_abc123
X-Device-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Body#

json
{
  "pin": "123456"
}
Field Type Required Rules
pin string Yes Exactly 6 digits, numeric only. No letters, spaces, or symbols.

Valid examples#

json
{ "pin": "123456" }
{ "pin": "000000" }
{ "pin": "999999" }

Invalid examples#

json
{ "pin": "12345" }      ← too short
{ "pin": "1234567" }    ← too long
{ "pin": "12345a" }     ← contains letter
{ "pin": " 12345" }     ← leading space
{}                      ← missing field

Responses#

200 OK — PIN verified#

The PIN is correct. Proceed with the void or refund.

json
{
  "success": true,
  "data": {
    "verified": true
  }
}

400 Bad Request — Validation error#

pin is missing or does not meet the format rules. Fix the input before retrying.

json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "pin is required and must be exactly 6 digits"
  }
}

401 Unauthorized — PIN not configured#

The merchant has not set a PIN yet. Show a message directing them to set a PIN in the portal.

json
{
  "success": false,
  "error": {
    "code": "PIN_NOT_CONFIGURED",
    "message": "Merchant PIN is not configured"
  }
}

401 Unauthorized — Wrong PIN#

The PIN format is valid but does not match the merchant's PIN. Show an error to the merchant and allow retry.

json
{
  "success": false,
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Invalid PIN"
  }
}

401 Unauthorized — Auth error#

The user JWT is missing, expired, or invalid. Re-authenticate the merchant. Through the API gateway an expired token returns TOKEN_EXPIRED ("Access token has expired. Please refresh your token.") — refresh it and retry.

json
{
  "success": false,
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Invalid or expired token"
  }
}

A wrong PIN and an auth failure both return 401 UNAUTHORIZED; tell them apart by error.message ("Invalid PIN") or by checking the JWT first.

401 Unauthorized — Device token invalid or revoked#

X-Device-Token was provided but is invalid or revoked. message is one of "Invalid device token", "Invalid token type", "Device session not found", "Device token has been revoked" or "Device not found". Re-register the device.

json
{
  "success": false,
  "error": {
    "code": "INVALID_DEVICE_TOKEN",
    "message": "Device token has been revoked"
  }
}

403 Forbidden — Device suspended#

The device has been suspended by an admin. Contact the merchant portal admin.

json
{
  "success": false,
  "error": {
    "code": "DEVICE_SUSPENDED",
    "message": "This device has been suspended"
  }
}

404 Not Found — No merchant#

The user JWT is not scoped to a merchant (for example a tenant-admin session), or the merchant no longer exists. Select or switch merchant first.

json
{
  "success": false,
  "error": {
    "code": "NOT_FOUND",
    "message": "Merchant not found"
  }
}

500 Internal Server Error#

Unexpected server error. Retry once; if it persists, contact backend team.

json
{
  "success": false,
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "PIN verification failed"
  }
}

Error Handling Guide#

HTTP error.code App action
400 VALIDATION_ERROR Show inline field error — invalid PIN format
401 UNAUTHORIZED "Invalid PIN" → allow merchant to retry; otherwise the user JWT is invalid → re-authenticate
401 PIN_NOT_CONFIGURED Show "PIN not set" — direct merchant to set PIN in portal
401 INVALID_DEVICE_TOKEN Device token invalid — trigger device re-registration
401 TOKEN_EXPIRED Refresh the user JWT and retry once
403 DEVICE_SUSPENDED Show suspension message — do not retry automatically
404 NOT_FOUND Session has no merchant — select or switch merchant
500 INTERNAL_ERROR Show generic error — retry once after short delay

Branch your error-handling logic on error.code. The one exception is 401 UNAUTHORIZED, which covers both a wrong PIN and an invalid user JWT.


Retry Policy#

  • Wrong PIN (401 UNAUTHORIZED): Allow the merchant to retry. The server does not limit or lock out failed attempts, so implement a UI counter to warn after 3 failed attempts. Do not auto-retry.
  • PIN not configured (401 PIN_NOT_CONFIGURED): Do not retry — show a message directing the merchant to set a PIN via the portal.
  • Validation error (400): Do not retry — fix the input first.
  • Server error (500): Retry once after 2–3 seconds.
  • Auth errors (401 token issues, 403): Do not retry automatically — require user action.

Integration Example (Dart / Flutter)#

dart
Future<bool> verifyMerchantPin(String pin) async {
  final response = await http.post(
    Uri.parse('$baseUrl/api/v1/merchants/verify-pin'),
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer $userJwt',
      'X-Tenant-ID': tenantId,
      if (deviceToken != null) 'X-Device-Token': deviceToken!,
    },
    body: jsonEncode({'pin': pin}),
  );

  if (response.statusCode == 200) {
    return true;
  }

  final body = jsonDecode(response.body);
  final code = body['error']?['code'];

  if (response.statusCode == 401 && code == 'UNAUTHORIZED') {
    throw InvalidPinException();
  }

  if (response.statusCode == 401 && code == 'PIN_NOT_CONFIGURED') {
    throw PinNotConfiguredException();
  }

  if (response.statusCode == 403 && code == 'DEVICE_SUSPENDED') {
    throw DeviceSuspendedException();
  }

  throw PinVerificationException(body['error']?['message']);
}

Integration Example (Kotlin / Android)#

kotlin
suspend fun verifyMerchantPin(pin: String): Boolean {
    val request = Request.Builder()
        .url("$baseUrl/api/v1/merchants/verify-pin")
        .addHeader("Content-Type", "application/json")
        .addHeader("Authorization", "Bearer $userJwt")
        .addHeader("X-Tenant-ID", tenantId)
        .apply { deviceToken?.let { addHeader("X-Device-Token", it) } }
        .post("""{"pin":"$pin"}""".toRequestBody("application/json".toMediaType()))
        .build()

    val response = client.newCall(request).await()
    val body = response.body?.string()?.let { Json.parseToJsonElement(it).jsonObject }

    val errorCode = body?.get("error")?.jsonObject?.get("code")?.jsonPrimitive?.content
    return when {
        response.code == 200 -> true
        response.code == 401 && errorCode == "PIN_NOT_CONFIGURED" -> throw PinNotConfiguredException()
        response.code == 401 -> throw InvalidPinException(body?.errorMessage())
        response.code == 403 -> throw DeviceSuspendedException()
        else -> throw PinVerificationException(body?.errorMessage())
    }
}

Changelog#

Date Change
2026-04-24 Full implementation shipped — real DB + bcrypt; added PIN_NOT_CONFIGURED case
2026-04-23 Initial stub implementation shipped