Skip to main content
readSignedInvoice activates the NFC reader, waits for a merchant’s HCE tag, validates the ECDSA signature and security constraints, and returns a verified PaymentRequest.

Import

import { readSignedInvoice } from '@taprails/tap-to-pay';

Signature

async function readSignedInvoice(): Promise<PaymentRequest>

Returns

A PaymentRequest object after passing all security checks:
interface PaymentRequest {
  paymentId: string;         // Payment ID (same as NFC tag's `id`)
  merchantWallet: string;    // Merchant wallet address
  amount: string;            // USDC decimal string, e.g. "25.00"
  currency: 'USDC';
  network: 'base';
  timestamp: number;         // Unix ms timestamp from invoice
  transactionId: string;     // Alias for paymentId
}

Security validation

Before returning, readSignedInvoice enforces three security checks and throws NFCPaymentError if any fail:
CheckConstraintError code
Expirynow > invoice.expiresAtINVALID_DATA
Timestamp windowInvoice is more than 5 minutes old (replay attack)INVALID_DATA
ECDSA signatureSignature must verify against merchant’s registered device keysINVALID_DATA
The merchant’s active device public keys are fetched from the backend (/api/v1/sdk/merchant-pubkey) and verified locally on the customer’s device.

Throws

ErrorCodeCondition
NFCPaymentErrorNOT_SUPPORTEDDevice has no NFC
NFCPaymentErrorNOT_ENABLEDNFC is turned off
NFCPaymentErrorTIMEOUTNo tag detected within 30 seconds
NFCPaymentErrorINVALID_DATAExpired, stale, malformed, or bad signature
NFCPaymentErrorUSER_CANCELLEDUser dismissed the NFC prompt
NFCPaymentErrorREAD_FAILEDGeneric NFC hardware failure

Example

import { readSignedInvoice, NFCPaymentError, NFCErrorCode } from '@taprails/tap-to-pay';

const handleScan = async () => {
  try {
    const payment = await readSignedInvoice();
    console.log('Amount:', payment.amount, 'USDC');
    console.log('Merchant:', payment.merchantWallet);
    // Proceed to processPayment...
  } catch (err) {
    if (err instanceof NFCPaymentError) {
      if (err.code === NFCErrorCode.TIMEOUT) {
        Alert.alert('Timed out', 'Move your phone closer and try again');
      } else {
        Alert.alert('Scan failed', err.message);
      }
    }
  }
};
useNFCCustomer.scanPaymentRequest() calls readSignedInvoice internally and surfaces the result/error via hook state. Prefer the hook over calling readSignedInvoice directly.