Skip to content
Tolinku
Tolinku
Sign In Start Free
Use Cases · · 5 min read

P2P Transfer Deep Links for Payment Apps

By Tolinku Staff
|
Tolinku fintech deep linking dashboard screenshot for use cases blog posts

Peer-to-peer payment apps like Venmo, Cash App, and Zelle use deep links to simplify money transfers. Instead of opening the app, finding the recipient, typing an amount, and adding a note, the user taps a single link and sees a pre-filled transfer form. This pattern reduces transfer friction and increases completion rates.

For the broader payment deep linking strategy, see Payment Deep Links. For the fintech overview, see Deep Linking for Fintech and Banking Apps.

The P2P Transfer Flow

  1. Open app
  2. Tap "Send Money"
  3. Search for recipient
  4. Enter amount
  5. Add note
  6. Confirm
  7. Authenticate (biometrics/PIN)

Seven steps. Each step is a potential drop-off.

  1. Tap deep link
  2. Review pre-filled transfer (recipient, amount, note)
  3. Confirm
  4. Authenticate

Four steps. The user's decision to send money was already made when they tapped the link; the app just needs confirmation and authentication.

URL Structure

Request Payment

When a user wants to receive money:

https://go.yourapp.com/send?to=@janedoe&amount=25.00&note=Coffee

Request Without Amount

Sometimes the recipient doesn't want to specify an amount (splitting a bill where each person's share varies):

https://go.yourapp.com/send?to=@janedoe&note=Dinner+split

The sender fills in their own amount.

Request by Identifier Type

Different identifier types:

By username:  /send?to=@janedoe
By phone:     /send?to=+15551234567
By email:     /[email protected]
By user ID:   /send?to=uid:user_abc123

Group Payment Request

For splitting among multiple people:

https://go.yourapp.com/split?from=@janedoe&total=120.00&count=4&note=Dinner

Each person owes $30.00. The link opens the app showing "Jane is requesting $30.00 for Dinner."

Implementation

function createPaymentRequestLink(recipient, amount, note) {
  const params = new URLSearchParams();
  params.set('to', recipient.username);
  if (amount) params.set('amount', amount.toFixed(2));
  if (note) params.set('note', note);

  return `https://go.yourapp.com/send?${params.toString()}`;
}

// Usage
const link = createPaymentRequestLink(
  { username: '@janedoe' },
  25.00,
  'Coffee'
);
// "https://go.yourapp.com/send?to=%40janedoe&amount=25.00&note=Coffee"
function handleSendDeepLink(url) {
  const parsed = new URL(url);
  const params = Object.fromEntries(parsed.searchParams);

  // Require authentication
  if (user.isAuthenticated === false) {
    pendingDeepLink.save(url);
    navigation.navigate('Auth');
    return;
  }

  // Resolve recipient
  const recipientId = params.to;
  const recipient = await resolveRecipient(recipientId);

  if (recipient === null) {
    navigation.navigate('RecipientNotFound', { identifier: recipientId });
    return;
  }

  // Parse amount
  const amount = params.amount ? parseFloat(params.amount) : null;

  // Navigate to send screen
  navigation.navigate('SendMoney', {
    recipient: recipient,
    amount: amount,
    note: params.note || '',
    isPreFilled: true,
  });
}

Recipient Resolution

Resolve the identifier to a real user:

async function resolveRecipient(identifier) {
  if (identifier.startsWith('@')) {
    return await lookupByUsername(identifier.substring(1));
  } else if (identifier.startsWith('+')) {
    return await lookupByPhone(identifier);
  } else if (identifier.includes('@')) {
    return await lookupByEmail(identifier);
  } else if (identifier.startsWith('uid:')) {
    return await lookupByUserId(identifier.substring(4));
  }
  return null;
}

Security Considerations

Recipient Verification

Before showing the transfer form, verify the recipient is real and active:

const recipient = await resolveRecipient(params.to);

if (recipient === null) {
  showError('This user was not found.');
  return;
}

if (recipient.accountStatus !== 'active') {
  showError('This user cannot receive payments at this time.');
  return;
}

// Show recipient details for the user to verify
// "Send $25.00 to Jane Doe (@janedoe)?"

Display the recipient's full name and profile photo so the sender can verify they're sending to the right person.

Amount Limits

Validate amounts against transaction limits:

const amount = parseFloat(params.amount);

if (isNaN(amount) || amount <= 0) {
  showError('Invalid amount');
  return;
}

if (amount > user.dailyTransferLimit) {
  showError(`Amount exceeds your daily transfer limit of $${user.dailyTransferLimit}`);
  return;
}

if (amount > user.balance) {
  navigation.navigate('InsufficientFunds', {
    required: amount,
    available: user.balance,
  });
  return;
}

Preventing Impersonation

A malicious actor could create a link with someone else's username as the recipient:

"Send me $100 for the tickets!"
https://go.yourapp.com/send?to=@attacker&amount=100.00¬e=Concert+tickets

The "to" field shows "@attacker" instead of the real friend. To prevent this:

  1. Always show the full recipient profile: Name, photo, username. The sender should recognize the recipient.
  2. Contacts integration: If the recipient is in the sender's contacts, show the contact name alongside the username.
  3. Warning for unknown recipients: If the sender has never transacted with this recipient before, show a warning: "You haven't sent money to this person before. Make sure you recognize them."

Payment request links should expire:

https://go.yourapp.com/send?to=@janedoe&amount=25.00&note=Coffee&exp=2026-05-01

After the expiration date, the app shows "This payment request has expired" instead of the transfer form. This prevents stale requests from being paid accidentally.

In-App Share Sheet

import { Share } from 'react-native';

async function sharePaymentRequest(amount, note) {
  const link = createPaymentRequestLink(user, amount, note);

  Share.share({
    message: `${user.displayName} is requesting $${amount.toFixed(2)} for "${note}". Pay here: ${link}`,
  });
}

QR Code

Generate a QR code for in-person payment requests:

// Display QR code in the app for another user to scan
const qrData = `https://go.yourapp.com/send?to=@${user.username}&amount=25.00&note=Coffee`;

NFC

For contactless payment requests, encode the deep link in an NFC tag. The payer taps their phone against the tag, and the transfer form opens.

When shared in messaging apps:

og:title: Jane is requesting $25.00
og:description: For "Coffee" via [Your App]
og:image: payment-request-card.png (branded payment request visual)

This makes the payment request look professional and trustworthy in message previews.

If the payer doesn't have the app:

  1. Payer taps the link
  2. Landing page shows: "Jane is requesting $25.00 for Coffee"
  3. Landing page has an "Install App" button and a "Pay on Web" option
  4. If the payer installs the app, deferred deep linking routes them to the pre-filled transfer form on first launch

The deferred deep link ensures the payment context survives the install process.

Measuring Performance

Metric Description
Request share rate % of users who share payment request links
Link tap rate % of shared links that are tapped
Authentication rate % of link taps that authenticate
Transfer completion rate % of authenticated sessions that complete the transfer
Average transfer amount Mean $ value of transfers from deep links
Time to complete Average seconds from link tap to transfer confirmed

For deep linking features, see Tolinku deep linking. For passing data through deep links, see Deep Link Parameters.

Get deep linking tips in your inbox

One email per week. No spam.

Ready to add deep linking to your app?

Set up Universal Links, App Links, deferred deep linking, and analytics in minutes. Free to start.